#!/usr/bin/lua -- Load external Lua modules local uci = require "uci" local ubus = require "ubus" local cjson = require "cjson" local posix = require "posix" local invoker = require "invoker" local tasklua = require "tasklua" local mosquitto = require "mosquitto" -- global variables local g_ucon = nil -- global ubus connection handle local g_saltval = nil -- unique integer salt value local g_intmqtt = nil -- internal MQTT connection handle local g_intokay = nil -- true if internal MQTT connection established local g_gwsn = nil -- gateway serialno. local g_syscfg = "/app/config/sysmonitor.json" local g_usbid = nil -- USB-id for 4G module local g_4ginfo = {} -- supported list of 4G modules local g_boardname = nil -- board name of gateway g_4ginfo['2c7c:0125'] = { ['ttynum'] = 2, ['name'] = 'quectel_EC2X', ['script'] = 'ec2x.gcom' } g_4ginfo['2c7c:6026'] = { ['ttynum'] = 2, ['name'] = 'quectel_EC200', ['script'] = 'ec200.gcom' } g_4ginfo['2c7c:6002'] = { ['ttynum'] = 2, ['name'] = 'quectel_EC200S', ['script'] = 'ec200.gcom' } g_4ginfo['2c7c:6005'] = { ['ttynum'] = 2, ['name'] = 'quectel_EC200S', ['script'] = 'ec200.gcom' } g_4ginfo['2c7c:0800'] = { ['ttynum'] = 2, ['name'] = 'quectel_RM500Q', ['script'] = 'rm500q.gcom' } g_4ginfo['2cb7:0a06'] = { ['ttynum'] = 0, ['name'] = 'fibocom_FG650_CN', ['script'] = 'FG650-CN_INIT.gcom' } g_4ginfo['1e0e:9001'] = { ['ttynum'] = 2, ['name'] = 'simcom_7600ce_first', ['script'] = 'sim7600_init.gcom' } g_4ginfo['1e0e:9011'] = { ['ttynum'] = 2, ['name'] = 'simcom_7600ce', ['script'] = 'sim7600_init.gcom' } g_4ginfo['1782:4058'] = { ['ttynum'] = 0, ['name'] = 'MH5001_5G', ['script'] = 'mh5001_5g.gcom' } g_4ginfo['2c7c:0900'] = { ['ttynum'] = 2, ['name'] = 'RG200U', ['script'] = 'rg200u.gcom' } g_4ginfo['1286:4e3d'] = { ['ttynum'] = 1, ['name'] = 'Air720', ['script'] = 'air720.gcom' } g_4ginfo['1782:4e00'] = { ['ttynum'] = 1, ['name'] = 'Air724UG', ['script'] = 'air720.gcom' } g_4ginfo['2dee:4d20'] = { ['ttynum'] = 1, ['name'] = 'meigSLM790', ['script'] = 'non-exist.gcom' } g_4ginfo['19d1:0001'] = { ['ttynum'] = 0, ['name'] = 'Air780E', ['script'] = 'air780.gcom' } local function ping_iface(hostp, netdev) local eval = invoker.invoke(invoker.NOSTDIO, "ping", "-c3", "-w3", "-I", netdev, hostp) if eval == 0 then return true end return false end local function ping_host(hostp, pcnt, wsec) if not pcnt then pcnt = 3 end if not wsec then wsec = 5 end local pid, rfd = invoker.invoke(invoker.NOSTDIO + invoker.OUTPUT + invoker.NOWAIT, "ping", string.format("-c%d", pcnt), string.format("-w%d", wsec), hostp) if not pid then io.stderr:write(string.format("Error, failed to ping '%s'\n", hostp)) io.stderr:flush() return false end tasklua.delay(wsec * 1000) local exited, eval = invoker.waitpid(pid, false) if exited ~= nil and not exited then io.stderr:write("Error, ping process still running...\n") io.stderr:flush() end if type(eval) == "number" and eval ~= 0 then posix.close(rfd); rfd = -1 return false end -- read output from PIPE local output = invoker.readfd(rfd, 4096) posix.close(rfd); rfd = -1 -- close pipe if type(output) ~= "string" then io.stderr:write("Error, failed to read output from ping.\n") io.stderr:flush() return false end if string.find(output, '100% packet loss', 1, true) then return false end return true end -- function to read and decode JSON configuration file local function load_json(jfile) local jdat = io.open(jfile, "rb") if not jdat then return nil end local jcfg = jdat:read("*a") jdat:close(); jdat = nil if not jcfg or #jcfg == 0 then return nil end local okay, cfgd = pcall(cjson.decode, jcfg) if not okay then io.stderr:write(string.format("Error, failed to decode '%s' as JSON!\n", jfile)) io.stderr:flush() return false end return cfgd end local function remove_largefile(filp, filz) local fst = posix.stat(filp) if type(fst) ~= "table" then return false end local fils = fst["st_size"] if type(fils) ~= "number" or fils < filz then return false end posix.unlink(filp) posix.sync() return true end local function write_file(jfile, jcfg, append) local jhdl = io.open(jfile, append and "ab" or "wb") if not jhdl then return false end if type(jcfg) == "string" then jhdl:write(jcfg) elseif type(jcfg) == "table" then jhdl:write(cjson.encode(jcfg)) end jhdl:close(); jhdl = nil return true end -- check whether given configuration is enabled local function check_config_enable(cfgfile, subsec) local rval = load_json(cfgfile) if type(rval) ~= "table" then return false end if subsec then rval = rval[subsec] end if type(rval) == "table" and rval.enable then return rval end return false end -- wait until config file is enabled local function wait_until_enable(cfgfile, subsec, chkival) local rval = nil if not chkival then -- default check every 5 seconds chkival = 5000 end while true do rval = load_json(cfgfile) if subsec and type(rval) == "table" then rval = rval[subsec] end if type(rval) == "table" and rval.enable then break end tasklua.delay(chkival) end return rval end local function is_yesterday(today, yday) if today == (yday + 1) then return true end if today > 0 then return false end if yday == 364 or yday == 365 then return true end return false end local function bandwidth_step(bcfg, bstore) local iface = bcfg["interface"] local netdev = bcfg["l3_device"] local typn = type(netdev) if typn ~= "string" or #netdev == 0 then io.stderr:write(string.format("Error, network device not found for '%s'\n", iface)) io.stderr:flush() return false end local rxbytes = invoker.readfile(string.format("/sys/class/net/%s/statistics/rx_bytes", netdev), invoker.TRIMEND) local txbytes = invoker.readfile(string.format("/sys/class/net/%s/statistics/tx_bytes", netdev), invoker.TRIMEND) if rxbytes then rxbytes = tonumber(rxbytes) end if txbytes then txbytes = tonumber(txbytes) end if not rxbytes or not txbytes then io.stderr:write(string.format("Failed to get rx/tx bytes for '%s'\n", netdev)) io.stderr:flush() return false end local nowt, uptim = os.time(), invoker.uptime() local datim = os.date('%Y%m%d-%H%M%S', nowt) local yday = string.match(os.date('%j', nowt), "([1-9]%d*)") if yday then yday = tonumber(yday) end if not yday then io.stderr:write("Error, failed to determine day of year!\n") io.stderr:flush() return false end local entry = { epoch = nowt, date = datim, ["yday"] = yday, uptime = uptim, salt = g_saltval, interface = iface, network = netdev, rx_bytes = rxbytes, tx_bytes = txbytes, total_bytes = rxbytes + txbytes, } local stored = load_json(bstore) if type(stored) ~= "table" then stored = {} end local nstore = #stored if nstore >= 1 then local last_entry = stored[nstore] local oldsalt = type(last_entry) == "table" and last_entry.salt or -1 local lastday = type(last_entry) == "table" and last_entry.yday or -1 if oldsalt == g_saltval and lastday == yday then local delta = rxbytes - last_entry["rx_bytes"] local deltim = uptim - last_entry["uptime"] delta = math.floor(delta + (txbytes - last_entry["tx_bytes"])) if delta >= 0x7FFFFFFF then io.stdout:write(string.format("Bandwith for '%s' in last %d seconds: %f bytes\n", iface, deltim, delta)) else io.stdout:write(string.format("Bandwith for '%s' in last %d seconds: %d bytes\n", iface, deltim, delta)) end io.stdout:flush() -- remove last entry: nstore = nstore - 1 end end stored[nstore + 1] = entry nstore = nstore + 1 if nstore > 1 then local rmtab = {} for idx, ent in ipairs(stored) do typn = type(ent) if typn == "table" then local theday = ent["yday"] or 0 if yday >= (theday + 2) then rmtab[#rmtab + 1] = idx end end end local minv, maxv = 1, #rmtab while minv < maxv do local tval = rmtab[minv] rmtab[minv] = rmtab[maxv] rmtab[maxv] = tval minv = minv + 0x1 maxv = maxv - 0x1 end for _, jdx in ipairs(rmtab) do table.remove(stored, jdx) end end -- write to bandwidth store file write_file(bstore, stored) posix.sync() local yestidx, firstidx, bwidth = -1, -1, {} for idx, ent in ipairs(stored) do typn = type(ent) if typn == "table" then local salt = ent["salt"] or 0 local theday = ent["yday"] or 0 if yday == theday then local bytes = ent["total_bytes"] if not bwidth[salt] or bwidth[salt] < bytes then bwidth[salt] = bytes end if firstidx < 0 then firstidx = idx end elseif is_yesterday(yday, theday) and yestidx < idx then yestidx = idx end end end local totbytes = 0 for _, bwd in pairs(bwidth) do totbytes = totbytes + bwd end if true then local yest = stored[yestidx] local first = stored[firstidx] if type(yest) == "table" and type(first) == "table" and yest.salt == first.salt then totbytes = totbytes - yest["total_bytes"] elseif type(first) == "table" and first.salt ~= g_saltval then totbytes = totbytes - first["total_bytes"] end end totbytes = math.floor(totbytes) if totbytes >= 0xFFFFFFFF then io.stdout:write(string.format("%s => bandwidth for '%s': %f bytes\n", os.date('%Y-%m-%d %H:%M:%S', nowt), iface, totbytes)) else io.stdout:write(string.format("%s => bandwidth for '%s': %d bytes\n", os.date('%Y-%m-%d %H:%M:%S', nowt), iface, totbytes)) end io.stdout:flush() return totbytes end local function bandwidth_stat(total, bcfg) local alevel = 0 for idx = 3, 1, -1 do local alarm = string.format("alarm_l%d", idx) local level = bcfg[alarm] if type(level) == "number" and total >= level then alevel = idx break end end local mqtt = g_intmqtt if not mqtt then io.stderr:write("Error, no internal MQTT connection found.\n") io.stderr:flush() return false end local topic = string.format("ipc/%s/sysmon/device/data/alarm", g_gwsn) local pmsg = { alarmType = 1, sn = g_gwsn, mi = 0, time = os.time(), tags = { [1] = { ["code"] = 0, ["status"] = (alevel > 0) and 1 or 2, ["limit"] = 0, ["level"] = aleval, ["data"] = string.format("Bandwidth statistics today: %.0f", total), }, }, } pmsg = cjson.encode(pmsg) mqtt:publish(topic, pmsg) return true end -- task entry for bandwidth statistics local function taskentry_bandwidth() -- emit a message to inform the start of bandwidth task io.stdout:write("TASK[bandwidth] started!\n") io.stdout:flush() -- bandwidth default configuration local bcfg, subsection = nil, 'bandwidth' -- bandwidth storage file local bandstore = "/app/bandwidth-statistics.json" bcfg = check_config_enable(g_syscfg, subsection) while not bcfg do bcfg = wait_until_enable(g_syscfg, subsection, 15000) end if true then -- determine the network device for the given interface local iface = bcfg["interface"] if type(iface) ~= "string" or #iface == 0 then iface = "wan" bcfg["interface"] = iface end local reply = g_ucon:call("network.interface." .. iface, "status", {}) if type(reply) ~= "table" then io.stderr:write("Error, ubus call has failed!\n") io.stderr:flush() return false end bcfg["l3_device"] = reply["l3_device"] end -- check bandwidth every 600 seconds local chkival = 600 * 1000 local nowtim = invoker.uptimsec() local nextim = nowtim -- mainloop while true do local totbytes = bandwidth_step(bcfg, bandstore) if totbytes then bandwidth_stat(totbytes, bcfg) end nowtim = invoker.uptimsec() nextim = nextim + chkival while nextim > nowtim do tasklua.delay(nextim - nowtim) nowtim = invoker.uptimsec() end if true then -- check whether bandwidth task is disabled local newcfg = check_config_enable(g_syscfg, subsection) if not newcfg then break end if newcfg["interface"] ~= bcfg["interface"] then posix.unlink(bandstore); posix.sync() io.stderr:write("Bandwidth interface has changed!\n") io.stderr:flush() break end end end return nil end local function intmqtt_connect(okay) if not okay then g_intokay = false io.stderr:write("Error, failed to connect to broker!\n") io.stderr:flush() return false end g_intokay = true local mqtt = g_intmqtt if mqtt then mqtt:subscribe('ipc/dummy/topic') end -- TODO: add interested topics here io.stderr:write("INFO: internal MQTT broker connected.\n") io.stderr:flush() return true end local function intmqtt_disconnect() g_intokay = false return true end local function intmqtt_message(msgid, msgtop, msgload) io.stdout:write(string.format("Received MQTT message: %s, payload: %s\n", type(msgtop) == "string" and msgtop or "nil", type(msgload) == "string" and msgload or "nil")) io.stdout:flush() return true end local function sysmon_init() posix.chdir('/') -- set process name invoker.setname('SYSMONITOR') -- initialize mosquitto library mosquitto.init() -- determine gateway board name g_boardname = invoker.readfile('/tmp/sysinfo/board_name', invoker.TRIMEND) if type(g_boardname) ~= "string" then g_boardname = "generic" end -- get gateway SN local gwsn = nil local eval, output = invoker.invoke(invoker.NOSTDIO + invoker.OUTPUT, "factory", "get") if eval == 0 and type(output) == "string" then gwsn = string.match(output, "SN=(%x+)") end if not gwsn then output = invoker.readfile("/app/config/fac.ini") if type(output) == "string" then gwsn = string.match(output, "sn%s*=%s*(%x+)") end end if not gwsn then io.stderr:write("Error, failed to determine gateway SN!\n") io.stderr:flush() return false end g_gwsn = gwsn io.stdout:write(string.format("Gateway SN: %s\n", gwsn)) io.stdout:flush() -- connect to ubus daemon g_ucon = ubus.connect(nil, 3) if not g_ucon then io.stderr:write("Error, failed to connect to ubus daemon!\n") io.stderr:flush() return false end local saltfile = "/tmp/sysmon-salt.txt" -- load random salt-value local rfile = io.open(saltfile, "rb") if rfile then local rval = rfile:read("*a") if rval and #rval > 0 then rval = tonumber(rval) end if rval then g_saltval = rval end rfile:close(); rfile = nil end if not g_saltval then -- generate random number of sysmon g_saltval = invoker.random(0x80000000) if not g_saltval then io.stderr:write("Error, failed to generate random number!\n") io.stderr:flush() return false end rfile = io.open(saltfile, "wb") if not rfile then io.stderr:write("Error, failed to open salt file!\n") io.stderr:flush() return false end rfile:write(tostring(g_saltval)) rfile:close(); rfile = nil end -- connect to internal MQTT broker local intmqtt = mosquitto.new() if not intmqtt then io.stderr:write("Error, failed to create MQTT handle\n") io.stderr:flush() return false end -- set mqtt callback functions intmqtt.ON_CONNECT = intmqtt_connect intmqtt.ON_DISCONNECT = intmqtt_disconnect intmqtt.ON_MESSAGE = intmqtt_message g_intmqtt = intmqtt local okay = intmqtt:connect() -- localhost MQTT broker if not okay then g_intmqtt = nil io.stderr:write("Error, failed to connect to internal MQTT broker!\n") io.stderr:flush() return false end -- bugfix of connection failure on x86_64/openwrt local nowtim = invoker.uptime() local nextim = nowtim + 0x3 while nowtim < nextim do intmqtt:loop(1000) -- wait 1000 msec nowtim = invoker.uptime() end if not g_intokay then g_intmqtt = nil intmqtt:destroy() io.stderr:write("Error, failed to connect to internal MQTT broker!\n") io.stderr:flush() return false end return true end local function mqtt_loop(ival) local mqtt = g_intmqtt if mqtt then local nowtim = invoker.uptimsec() local nextim = nowtim + ival while nowtim < nextim do mqtt:loop(math.floor(nextim - nowtim)) nowtim = invoker.uptimsec() end return true end invoker.msleep(ival) return true end local function pidof(appname) local eval, output = invoker.invoke(invoker.OUTPUT, "pidof", appname) if eval ~= 0 then return false end if not output or #output == 0 then io.stderr:write("wpa_supplicant not running\n") io.stderr:flush() return false end local rval, count = {}, 0 for pid in string.gmatch(output, "(%d+)") do count = count + 1 rval[count] = tonumber(pid) -- print(string.format("PID[%d]: %s", count, pid)) end if count == 0 then return false end return rval end local function find_nextarg(wpid, parg) local cmdline = string.format("/proc/%d/cmdline", wpid) local output = invoker.readfile(cmdline, 0, 4096) if not output or #output == 0 then return false end local lastarg, nextarg = nil, nil for argv in string.gmatch(output, "([^%z]+)") do if lastarg == parg then nextarg = argv break end lastarg = argv end return nextarg end local function find_ipaddrs(netdev) local eval, output = invoker.invoke(invoker.OUTPUT + invoker.NOSTDIO, "ip", "addr", "show", "dev", netdev) if eval ~= 0 then io.stderr:write(string.format("Error, ipaddr(%s) has failed: %s\n", netdev, tostring(eval) or tostring(output) or "unknown")) io.stderr:flush() return false end local addrs, count = {}, 0 for addr in string.gmatch(output, "%sinet%s+([.%d]+)/") do count = count + 1 addrs[count] = addr -- print(string.format("IPv4[%d]:\t%s", count, addr)) end if count == 0 then return false end return addrs end local function apstation_disable_station(wpa_pid) local wpacfg = find_nextarg(wpa_pid, "-c") if not wpacfg then io.stderr:write(string.format("Error, WPA configuration not found: %d\n", wpa_pid)) io.stderr:flush() return false end local output = invoker.readfile(wpacfg) if not output or #output == 0 then io.stderr:write(string.format("Error, failed to read WPA configuration file!\n")) io.stderr:flush() return false end local ssid = string.match(output, '%s+ssid="([^"]+)"') if not ssid then io.stderr:write("Error, ssid not found:\n") io.stderr:write(output) io.stderr:write("\n") io.stderr:flush() return false end io.stdout:write(string.format("WiFi station SSID: %s\n", ssid)) io.stdout:flush() local ifaces, count = {}, 0 local wifi = uci.cursor() wifi:foreach('wireless', 'wifi-iface', function (optval) local dotname = optval[".name"] if type(dotname) == "string" then count = count + 1 ifaces[count] = dotname end end) if count == 0 then io.stderr:write("Error, not WiFi configuration found.\n") io.stderr:flush() end local iface = nil -- name of /etc/config/wireless configuration section for _, ifname in ipairs(ifaces) do local sid = wifi:get('wireless', ifname, 'ssid') if type(sid) == "string" and sid == ssid then if 'sta' == wifi:get('wireless', ifname, 'mode') then iface = ifname break end end end if not iface then io.stderr:write(string.format("Error, config for ssid not found: %s\n", ssid)) io.stderr:flush() return false end -- following two lines cannot be used: -- wifi:set('wireless', iface, "disabled", "1") -- wifi:commit('wireless'); wifi = nil wifi = nil -- release wifi configuration -- use command-line uci instead, without `commit, -- /etc/config/wireless will not be modified: invoker.invoke(invoker.NOSTDIO, 'uci', 'set', string.format('wireless.%s.disabled=1', iface)) invoker.invoke(invoker.NOSTDIO, 'wifi', 'reload') return true end local function apstation_check(winfo, expire) local wpid = pidof('wpa_supplicant') if not wpid then if winfo.wpapid > 0 then winfo.wpapid = 0 end -- io.stderr:write("INFO: wpa_supplicant not running\n") -- io.stderr:flush() return false end wpid = wpid[1] if winfo.wpapid ~= wpid then winfo.wpapid = wpid winfo.wpatime = invoker.uptime() winfo.wpaiface = find_nextarg(winfo.wpapid, "-i") if not winfo.wpaiface then winfo.wpaiface = "unknown" end io.stdout:write(string.format("WPA process, PID: %d, iface: %s\n", wpid, winfo.wpaiface)) io.stdout:flush() return false end local ipaddrs = find_ipaddrs(winfo.wpaiface) if ipaddrs then winfo.wpatime = invoker.uptime() return false end local nowt = invoker.uptime() if nowt >= (winfo.wpatime + expire) then winfo.wpatime = nowt io.stderr:write(string.format("No ipv4 address found for '%s', disabling...\n", winfo.wpaiface)) io.stderr:flush() apstation_disable_station(wpid) return true end -- io.stderr:write(string.format("No ipv4 found for '%s'...\n", winfo.wpaiface)) -- io.stderr:flush() return false end local function taskentry_apstation() local apsta_config = wait_until_enable(g_syscfg, "apstation", 60000) if type(apsta_config) ~= "table" then return false end local ival = apsta_config["expire"] if type(ival) ~= "number" then ival = 120 else ival = math.floor(ival) end if ival < 15 or ival > 86400 then io.stderr:write(string.format("Error, invalid WiFi AP/STA expire time: %s\n", tostring(ival))) io.stderr:flush() ival = 120 end local nowt = invoker.uptimsec() local nextim = nowt local wpainfo = { ["wpapid"] = 0, ["wpaiface"] = false, ["wpatime"] = false, } while true do nextim = nextim + 30000 while nowt < nextim do tasklua.delay(nextim - nowt) nowt = invoker.uptimsec() end local hostapd_pid = pidof('hostapd') if hostapd_pid and apstation_check(wpainfo, ival) then break end if not hostapd_pid and wpainfo["wpapid"] > 0 then wpainfo["wpapid"] = 0 end end return nil end local function check_4gusbid() -- Enumerate output from `lsusb, -- and determine the 4G modem USB ID local okay, outbuf = invoker.invoke(invoker.OUTPUT + invoker.NOSTDIO, "lsusb") if okay ~= 0 or type(outbuf) ~= "string" or string.len(outbuf) == 0 then -- io.stderr:write("Error, failed to invoke `lsusb!\n") -- io.stderr:flush() return false end -- process line by line for output in string.gmatch(outbuf, "([^\r\n]+)") do local usbid = string.match(output, "(%x%x%x%x:%x%x%x%x)") if usbid and g_4ginfo[usbid] then g_usbid = usbid return true end end return false end local function get_vid_pid(device_path) -- get the Vendor and Product ID from the real device path local vid, pid local path = device_path repeat local uevent_file = io.open(path .. "/uevent") -- make sure the V and P id is saved in uevent, of course uedvadm also will get the information from uevent file if uevent_file then for line in uevent_file:lines() do local key, value = line:match("(%a+)=(.+)") if key == "PRODUCT" then vid, pid = value:match("(%x+)%/(%x+)%/(%x+)") if vid and pid then vid = string.format("%04x", tonumber(vid, 16)) pid = string.format("%04x", tonumber(pid, 16)) return vid .. ":" .. pid end end end uevent_file:close() end path = path:match("(.+)/[^/]+") -- if not found, next step is to check the parent directory. until not path or path == "" return nil, nil end local function find_device_path(device_name) local device_path -- unfortunately, this method is strong depends on kernel version. As far as I know it can't work on 2.4 kernel. for class in io.popen("ls " .. "/sys/class/"):lines() do local path = "/sys/class/" .. class .. "/" .. device_name if io.open(path) then local link = io.popen("readlink -f " .. path):read("*line") -- get the link path if link then while link:match("%.%./") do link = link:gsub("/[^/]+/%.%./", "/") -- get the real path end device_path = link break end end end return device_path end -- function to determine the ttyUSB device for 4G modem local function fetch_4g_ttyusb(info4) local idx, jdx, tnum = 0, 0, info4['ttynum'] local serial_dirs = { [1] = '/sys/bus/usb-serial/drivers/ch341-uart/', [2] = '/sys/bus/usb-serial/drivers/cp210x/', } while idx <= 32 do local ttydev = string.format('ttyUSB%d', idx) if posix.access('/dev/' .. ttydev) == 0 then local isserial = false for _, sdir in ipairs(serial_dirs) do if posix.access(sdir .. ttydev) == 0 then isserial = true break end end if not isserial then if jdx >= tnum then return string.format('/dev/%s', ttydev) end jdx = jdx + 1 end end idx = idx + 1 -- for next /dev/ttyUSBx end idx = 0 while idx <= 32 do local devname = string.format ("ttyACM%d", jdx) local ttydev = find_device_path (devname) local vpid = get_vid_pid (ttydev) if vpid == g_usbid then if posix.access('/dev/' .. devname) == 0 then return string.format('/dev/%s', devname) end end idx = idx + 1 -- for next /dev/ttyACMx end return nil end local function invoke_comgt(info4) local tdev = info4['ttydev'] or '/dev/null' if posix.access(tdev) == 0 then posix.setenv('TTY_READ', '0') io.stdout:write(string.format("Resetting %s...\n", tdev)) io.stdout:flush() -- Due to not enough tests, and no avaliable testing environments, -- it has not been confirmed that resetting 4G modem could solve network issues. invoker.invoke(invoker.NOSTDIO, 'tty_rw', tdev, "AT+RESET\r\n") tasklua.delay(25000) -- delay 25 seconds end io.stdout:write(string.format("Invoking comgt for %s...\n", info4['name'])) io.stdout:flush() local pid = invoker.invoke(invoker.NOWAIT + invoker.CLOSEFD + invoker.NOSTDIO, "comgt", "-d", tdev, '-s', string.format('/etc/gcom/%s', info4['script'])) if not pid then io.stderr:write("Error, failed to invoke comgt!\n") io.stderr:flush() return false end while true do tasklua.delay(5000) -- delay 5 seconds local done, eval = invoker.waitpid(pid, true) if done == nil then break end if done then io.stdout:write(string.format("comgt has exited with: %d\n", type(eval) == "number" and eval or 1)) io.stdout:flush() break end end return true end local function taskentry_4gnetwork() local tfile = '/tmp/4g_modem_tty' local mfile = '/app/config/4g_modem' local info4g = nil -- fetched from `g_4ginfo -- for fresh boot, until 60 seconds if invoker.uptime() < 90 then local upt = invoker.uptime() upt = (94 - upt) * 1000 if upt > 0 then tasklua.delay(upt) end else tasklua.delay(4000) -- delay 4 seconds end local icount = 0 -- check only for 4 times while icount < 4 do icount = icount + 1 local docomgt, have4g = false, false if check_4gusbid() then -- 4G module found have4g = true info4g = g_4ginfo[g_usbid] -- check the content of /app/config/4g_modem local m4g = invoker.readfile(mfile, invoker.TRIMEND) if m4g and m4g ~= info4g['name'] then docomgt = true -- invoke comgt local hdl = io.open(mfile, "wb") if hdl then -- `mfile has incorrect name, update it! hdl:write(string.format("%s\n", info4g['name'])) hdl:close(); hdl = nil; posix.sync() end end -- get the real ttyUSB device for 4G modem local t4g0 = fetch_4g_ttyusb(info4g) if not t4g0 then docomgt = false -- invoke comgt io.stderr:write("Error, failed to determine ttyUSB for 4G modem.\n") io.stderr:flush() else info4g['ttydev'] = t4g0 -- save 4g_modem_tty -- check the content of /tmp/4g_modem_tty local t4g1 = invoker.readfile(tfile, invoker.TRIMEND) if t4g0 ~= t4g1 then -- update /tmp/4g_modem_tty local hdl = io.open(tfile, "wb") if hdl then hdl:write(string.format("%s\n", t4g0)) hdl:close(); hdl = nil; posix.sync() end docomgt = true -- invoke comgt end end end if docomgt or (have4g and not ping_host('mqtt.lnxall.com')) then invoke_comgt(info4g) end tasklua.delay(90000) -- delay 90 seconds end io.stdout:write("Task for 4G network will now exit!\n") io.stdout:flush() return false end local function network_update_metric(iface, mval) local ncfg = uci.cursor('/etc/config') local oval = ncfg:get("network", iface, 'metric') mval, oval = tostring(mval), tostring(oval) if oval == mval then return false end ncfg:delete("network", iface, 'metric') ncfg:delete("network", iface, 'metric') ncfg:set("network", iface, 'metric', mval) ncfg:commit("network"); ncfg = nil -- recheck set metric ncfg = uci.cursor('/etc/config') oval = ncfg:get("network", iface, 'metric') ncfg = nil -- free object reference if oval == nil then -- bugfix of `unconditionally reload of `/etc/config/network -- The `iface might not exist in `/etc/config/network, so -- nothing has changed, thus `false should be returned. return false end return true end local function network_get_priority(ucon, pcfgs) local oklist, okcnt = {}, 0 local ipaddr = pcfgs["pingipaddr"] local ifaces = pcfgs["priorities"] local metric_values = { 0, 10, 20, 30 } for _, iface in ipairs(ifaces) do local reply = ucon:call('network.interface', 'status', {["interface"] = iface}) if not reply or type(reply) ~= "table" then io.stderr:write(string.format("Warning, no status found for '%s'\n", iface)) io.stderr:flush() else local ndev = reply["l3_device"] if type(ndev) == "string" and #ndev > 0 and ping_iface(ipaddr, ndev) then okcnt = okcnt + 1 oklist[iface] = metric_values[okcnt] end end end if okcnt == 0 then -- network interfaces all down, no network connection return false end for _, iface in ipairs(ifaces) do if not oklist[iface] then okcnt = okcnt + 1 oklist[iface] = metric_values[okcnt] end end return oklist end local function taskentry_netpriority() local cfgdat = check_config_enable(g_syscfg, 'netpriority') if type(cfgdat) ~= "table" then tasklua.delay(600 * 1000) -- delay 10 minutes return true -- end current task end local pcfgs, idx = {}, 1 -- re-process the priority configs pcfgs["pingipaddr"] = cfgdat["pingipaddr"] local nlist, plist = {}, cfgdat["priorities"] if type(plist) ~= "table" then return false end while idx <= 4 do local iface = plist[idx] if type(iface) == "string" and #iface > 0 then nlist[#nlist + 1] = iface end idx = idx + 1 end pcfgs["priorities"] = nlist if #nlist == 0 then tasklua.delay(600 * 1000) return false -- end current task, it will restart again end -- main loop for network priority detection while true do plist = network_get_priority(g_ucon, pcfgs) if plist then local reload = false for iface, metval in pairs(plist) do if network_update_metric(iface, metval) then reload = true end -- io.stdout:write(string.format("Metric update: '%s' => %d\n", iface, metval)) -- io.stdout:flush() end if reload then io.stdout:write("Reloading network for new priorities...\n") io.stdout:flush() invoker.invoke(invoker.NOSTDIO, '/etc/init.d/network', 'reload') end end tasklua.delay(60 * 1000) end return false end -- to replace shell script, `/lib/dyiot/bin/supervisor_4g.sh local function taskentry_super4g() local gcfgs = load_json(g_syscfg) if type(gcfgs) ~= "table" then gcfgs = {} end local super4g = gcfgs['supervisor_4g'] if type(super4g) == "table" and not super4g["enable"] then tasklua.delay(240 * 1000) -- delay 4 minutes return true end io.stdout:write("Network supervisor 4G task started!\n") io.stdout:flush() -- try to determine MQTT server local mcfg = load_json('/app/config/mqtt_server.json') if not mcfg then io.stderr:write("Error, failed to load mqtt_server.json\n") io.stderr:flush() tasklua.delay(240 * 1000) return true end local mserver, mport = mcfg["host"], mcfg["port"] if type(mport) == "string" then mport = tonumber(mport) end if type(mserver) ~= "string" or type(mport) ~= "number" then io.stderr:write("Error, failed to determine MQTT server and port\n") io.stderr:flush() tasklua.delay(240 * 1000) return true end mcfg = nil -- free memory allocation local wan_idx = -1 -- WAN index for MT7621/MT7628 local wan_state = false -- previous state of WAN -- determine swconfig command if g_boardname == 'WOOLINK-MT7621-512M-256M' then wan_idx, wan_state = 4, 'up' tasklua.delay(15 * 1000) -- delay 15 seconds elseif g_boardname == 'WOOLINK-MT7628-128M-32M' then wan_idx, wan_state = 0, 'up' tasklua.delay(15 * 1000) -- delay 15 seconds end -- local variables definition local misscnt = 0 -- count of network failure local CHECK_INTERVAL = 30 -- check network every 30 seconds local nowtim = invoker.uptime() local nextim = nowtim + CHECK_INTERVAL -- task main loop while true do local okay, refused = invoker.tcpcheck(mserver, mport, 1500) -- print(os.date(), 'Checking network:', okay, refused) if refused then okay = true end if not okay then misscnt = misscnt + 0x1 io.stdout:write(string.format("supervisor 4g miss count: %d\n", misscnt)) io.stdout:flush() elseif misscnt > 0 then misscnt = 0 end if wan_idx >= 0 then -- refer to `check_ports function in `/lib/dyiot/bin/supervisor_4g.sh local res, stbuf = invoker.invoke(invoker.OUTPUT + invoker.CLOSEFD, 'swconfig', 'dev', 'switch0', 'port', tostring(wan_idx), 'get', 'link') if res == 0 and type(stbuf) == "string" then local wstate = string.match(stbuf, "link:([^%s]+)") if wstate and string.len(wstate) > 0 and wan_state ~= wstate then wan_state = wstate local ndev = string.format('eth0.409%d', wan_idx) io.stdout:write(string.format("Up and down for network '%s'\n", ndev)) io.stdout:flush() -- why `ifconfig XXX down/up ? -- I don't know, just refer to `/lib/dyiot/bin/supervisor_4g.sh invoker.invoke(invoker.NOSTDIO, 'ifconfig', ndev, 'down') invoker.invoke(invoker.NOSTDIO, 'ifconfig', ndev, 'up') end end end -- check the miss count if misscnt == 120 then -- needed to reset 4G modem io.stderr:write("Resetting 4G modem...\n"); io.stderr:flush() invoker.invoke(invoker.CLOSEFD, 'pcie_rst') invoker.invoke(invoker.CLOSEFD, 'reset_modem') elseif misscnt > 150 then -- need to restart gateway, backup database invoker.invoke(invoker.NOSTDIO + invoker.CLOSEFD, "/bin/sh", "-c", '/etc/init.d/cloud_mqtt stop ; killall cloud_mqtt') posix.unlink('/app/REPORT_DB.tar.gz') -- delete it first? tasklua.delay(5000) -- just as the `sleep 5 from `/lib/dyiot/bin/supervisor_4g.sh invoker.invoke(invoker.NOSTDIO + invoker.CLOSEFD, "/bin/sh", "-c", 'cd /tmp ; tar -czvf /app/REPORT_DB.tar.gz *_REPORT_*.db*') -- log the reboot information local logf = '/app/network_failure_reboot.log' remove_largefile(logf, 131072) local logh = io.open(logf, "a") if logh then logh:write(string.format("Network failure, uptime: %d, reboot at '%s'\n", invoker.uptime(), os.date())) logh:close(); logh = nil end posix.sync(); invoker.invoke(invoker.NOSTDIO, 'reboot') break end -- time to yield task execution nowtim = invoker.uptime() while nowtim < nextim do tasklua.delay((nextim - nowtim) * 1000) nowtim = invoker.uptime() end nextim = nextim + CHECK_INTERVAL if nextim <= nowtim then nextim = nowtim + CHECK_INTERVAL end end tasklua.delay(20 * 1000) return true end -- Refer to `check_critical_processes function from `/lib/dyiot/bin/supervisor.sh local function taskentry_critical() local procs = { [1] = 'frpc', [2] = 'frpc', [3] = 'rathole', [4] = 'rathole' } local idx, plen = 0x1, #procs local iflags = invoker.NOSTDIO + invoker.OUTPUT + invoker.CLOSEFD while true do local eval, output = invoker.invoke(iflags, "pgrep", procs[idx]) if type(output) ~= "string" then output = "" end local initd = '/etc/init.d/' .. procs[idx + 1] if (eval ~= 0 or #output == 0) and posix.access(initd) == 0 then io.stderr:write(string.format("%s is abnormal, restarting...\n", procs[idx])) io.stderr:flush() invoker.invoke(iflags, initd, 'restart') end idx = idx + 0x2 if idx > plen then idx = 0x1 end tasklua.delay(60000) -- delay for 60 seconds end return true end local function meminfo_available() -- load /proc/meminfo local meminfo = invoker.readfile('/proc/meminfo') if not meminfo then meminfo = "" end local mava = string.match(meminfo, "MemAvailable:%s+(%d+)") if mava then mava = tonumber(mava) end if not mava then mava = 1024 end return meminfo, mava end -- refer to function check_sysstat from `/lib/dyiot/bin/supervisor.sh local function taskentry_sysstat() tasklua.delay(60000) -- delay 60 seconds first local iflags = invoker.OUTPUT + invoker.NOSTDIO + invoker.CLOSEFD while true do local okay, topinfo, dfinfo = nil, nil, nil -- get process running information -- iflags + 0x30000 => output buffer needs to be large okay, topinfo = invoker.invoke(iflags + 0x30000, "top", "-n1", "-b") if okay ~= 0 or type(topinfo) ~= "string" then topinfo = "" end -- get filesystem information okay, dfinfo = invoker.invoke(iflags, 'df') if okay ~= 0 or type(dfinfo) ~= "string" then dfinof = "" end -- extract free size for tmpfs local tmp_free = string.match(dfinfo, "tmpfs%s+%d+%s+%d+%s+(%d+)") if tmp_free then tmp_free = tonumber(tmp_free) end if not tmp_free then tmp_free = 2048 end -- extract free flash size local flash_free = string.match(dfinfo, "overlayfs[^%s]+%s+%d+%s+%d+%s+(%d+)") if not flash_free then -- overlayfs not found, try `/dev/root flash_free = string.match(dfinfo, "/dev/root%s+%d+%s+%d+%s+(%d+)") end if flash_free then flash_free = tonumber(flash_free) end if not flash_free then flash_free = 2048 end local meminfo, mem_free = meminfo_available() -- emit a message, just like `check_sysstat io.stdout:write(string.format("tmp_free: %d, flash_free: %d, mem_free: %d\n", tmp_free, flash_free, mem_free)) io.stdout:flush() if mem_free < 4096 then -- read /proc/meminfo again after flushing caches write_file('/proc/sys/vm/drop_caches', "3\n") invoker.msleep(300) -- delay 0.3 second meminfo, mem_free = meminfo_available() end if mem_free < 4096 then write_file('/app/top_info', topinfo) write_file('/app/meminfo', meminfo) invoker.invoke(invoker.NOSTDIO, '/bin/sh', '-c', "find /proc -name status | xargs cat >/app/proc_status") io.stdout:write("reboot as less sys resource.\n") io.stdout:flush() tasklua.delay(10000) invoker.invoke(invoker.NOSTDIO, 'reboot') end if flash_free < 4096 then invoker.invoke(invoker.NOSTDIO + invoker.CLOSEFD, '/bin/sh', '-c', "exec du /app > /app/du_info") invoker.invoke(invoker.NOSTDIO, '/bin/sh', '-c', 'exec rm -rf /app/*.log /app/*.tar.gz') invoker.invoke(invoker.NOSTDIO, 'sqlite3', '/app/collect.db', "delete from data_table where report=1;") for idx = 0, 4 do invoker.invoke(invoker.NOSTDIO, 'sqlite3', string.format('/app/MQTT_REPORT_%d.db', idx), "delete from data_table where report=1;") end posix.sync() end if tmp_free < 4096 then write_file('/app/df_info', dfinfo) io.stdout:write("reboot as less tmp resource\n") io.stdout:flush() tasklua.delay(10000) invoker.invoke(invoker.NOSTDIO, 'reboot') end tasklua.delay(60000) end return false end local function taskentry_coredump() tasklua.delay(60000) -- delay 60 seconds before continue local dorm = false while true do invoker.invoke(invoker.NOSTDIO, '/bin/sh', '-c', 'exec rm -rf /tmp/gcom.*.core') local corefiles = posix.glob('/tmp/*.core') if type(corefiles) == 'table' then dorm = true local count = 0 for _, corefile in ipairs(corefiles) do local cmd = corefile:sub(6) cmd = string.format('cd /tmp; exec tar czvf /app/%s.tar.gz "%s" /etc/issue', cmd, cmd) -- io.stdout:write(string.format('%s\n', cmd)); io.stdout:flush() invoker.invoke(invoker.NOSTDIO + invoker.LOWPRI, '/bin/sh', '-c', cmd) posix.unlink(corefile) -- delete corefile count = count + 0x1 if count >= 10 then break end end -- remove all core files invoker.invoke(invoker.NOSTDIO, '/bin/sh', '-c', 'exec rm -rf /tmp/*.core') posix.sync() end if dorm then -- remove compressed core files local corecnt = 60 local corefiles = posix.glob('/app/*.core.tar.gz') if corefiles and #corefiles > corecnt then while true do corecnt = corecnt + 0x1 local cfile = corefiles[corecnt] if type(cfile) ~= "string" then break end -- io.stdout:write(string.format('Unlinking %s...\n', cfile)); io.stdout:flush() posix.unlink(cfile) end posix.sync() end dorm = false end tasklua.delay(60000) -- delay 60 seconds end end -- refer to `check_can function from `/lib/dyiot/bin/supervisor.sh -- is this really necessary? local function taskentry_candev() local devs = posix.glob('/sys/class/net/can*') if not devs then devs = {} io.stderr:write("Error, no can device found!\n") io.stderr:flush() end local iflags = invoker.CLOSEFD + invoker.NOSTDIO while true do tasklua.delay(120000) for _, cdev in ipairs(devs) do local stat = invoker.readfile(cdev .. '/operstate', invoker.TRIMEND) if stat == 'down' then local candev = string.match(cdev, "/([^/]+)$") if candev then -- io.stdout:write("try to bring up " .. candev .. "...\n") -- io.stdout:flush() invoker.invoke(iflags, 'ifconfig', candev, 'down') tasklua.delay(1000) invoker.invoke(iflags, 'ifconfig', candev, 'up') else io.stderr:write(string.format("Error, invalid CAN path: %s\n", cdev)) io.stderr:flush() end end end end return true end -- refer to `rootfs_ro_check function from `/lib/dyiot/bin/supervisor.sh local function taskentry_readonly() local logfile = '/boot/rootfs-readonly.log' while true do local okay, output = invoker.invoke(invoker.OUTPUT, 'grep', '-e', '/dev/root / ext4', '/proc/mounts') if okay == 0 and type(output) == 'string' then if string.find(output, 'rw,', 1, true) then io.stdout:write("rootfs read-write.\n") io.stdout:flush() elseif string.find(output, 'ro,', 1, true) then io.stdout:write("rootfs read-only.\n") io.stdout:flush() remove_largefile(logfile, 0x10000) write_file(logfile, string.format("%s: rootfs readonly! Trying to recover...\n", os.date()), true) posix.sync() invoker.invoke(invoker.CLOSEFD, 'fsck.ext4', '-y', '/dev/sda2') posix.sync(); tasklua.delay(5000) invoker.invoke(0, 'reboot') end end tasklua.delay(120000) end return true end local function sysmon_main() posix.setenv('PATH', '/lib/dyiot/bin:/usr/sbin:/usr/bin:/sbin:/bin', 1) if not sysmon_init() then invoker.msleep(5000) return false end -- NOTE: add tasks here: tasklua.taskadd(taskentry_bandwidth, true) tasklua.taskadd(taskentry_apstation, true) tasklua.taskadd(taskentry_4gnetwork, false) tasklua.taskadd(taskentry_netpriority, true) tasklua.taskadd(taskentry_super4g, true) tasklua.taskadd(taskentry_critical, true) tasklua.taskadd(taskentry_sysstat, true) tasklua.taskadd(taskentry_coredump, true) if posix.access('/sys/class/net/can0') == 0 or posix.access('/sys/class/net/can1') == 0 then tasklua.taskadd(taskentry_candev, true) end local unam, mounts = posix.uname(), false if type(unam) == "string" and string.find(unam, 'x86_64', 1, true) then mounts = invoker.readfile('/proc/mounts') elseif type(unam) == "table" and unam.machine == 'x86_64' then mounts = invoker.readfile('/proc/mounts') end if mounts and string.find(mounts, '/dev/root / ext4', 1, true) and posix.access('/dev/sda2') == 0 then tasklua.taskadd(taskentry_readonly, true) end unam, mounts = nil, nil -- delete local variables -- loop until internal MQTT connections breaks while g_intokay do tasklua.taskloop(1800 * 1000, mqtt_loop) collectgarbage() end return false end sysmon_main() os.exit(1)