#!/opt/lnxall_app/bin/lua -- Created by jiaqiang.ye@lnxall.com -- Simple Phony INIT utility -- 2023/03/06 -- load external Lua modules local ubus = require "ubus" local cjson = require "cjson" local posix = require "posix" local uloop = require "uloop" local invoker = require "invoker" -- global constants local LINIT_APPDIR = '/opt/lnxall_app/app' local LINIT_CONFIG = '/app/config/lnxall_applist.json' local LINIT_SYSTEM = '/opt/lnxall_app/platspec/linit_system.sh' local LINIT_CHKINVAL = 30000 -- every 30 seconds -- directory for application disable tag files: local LINIT_APPDISABLE = '/lib/dyiot/appdis' local UBUS_FUNCS = {} -- ubus service functions -- global variables local g_appmap = { } -- application container local g_applist = { } -- application list local g_looping = true -- global looping flag local g_timer = nil -- global uloop timer handle local g_timestamp = nil -- timer function stamp value local g_sigchild = false -- mark whether received SIGCHLD local g_ucon = nil -- ubus connection handle local function sigint_handler(signo) if signo == posix.SIGINT then g_looping = false uloop.cancel() elseif signo == posix.SIGCHLD then g_sigchild = true else io.stderr:write(string.format("Error, unknown signal received: %d\n", signo)) io.stderr:flush() end end local function decode_appname(reqmsg, ubusmsg) local appname = type(ubusmsg) == "table" and ubusmsg["appname"] or nil if type(appname) ~= "string" or #appname == 0 then g_ucon:reply(reqmsg, { ["error"] = 1, errmsg = "no appname specified." }) return nil end if type(g_appmap[appname]) ~= "table" then g_ucon:reply(reqmsg, { ["error"] = 2, errmsg = string.format("appname not found: '%s'", appname) }) return nil end return appname end local function app_is_enabled(appname) local tagfile = string.format("%s/%s", LINIT_APPDISABLE, appname) if posix.access(tagfile) == 0 then return false end return true end local function doapp_stop(appname) local jobn = g_appmap[appname] local pid = jobn.pidrun if pid and pid > 0 then local rval, errmsg = invoker.kill(pid, posix.SIGKILL) if not rval then if type(errmsg) ~= "string" then errmsg = tostring(errmsg) end io.stderr:write(string.format("Error, kill '%s' => %s\n", appname, errmsg)) io.stderr:flush() end invoker.waitpid(pid, false) jobn.pidrun = 0 end jobn.restart = false return true end local function doapp_start(appname) local jobn = g_appmap[appname] local pid = jobn.pidrun if pid and pid > 0 then return true end -- application already happily running local nrun = jobn.numrun + 0x1 if nrun >= 0x7FFFFFFF then nrun = 0 end local flag = invoker.NOWAIT + invoker.CLOSEFD if jobn.nostdio then flag = flag + invoker.NOSTDIO end local newpid, errmsg = invoker.invoke(flag, jobn.argv) if newpid then jobn.numrun = nrun jobn.pidrun = newpid io.stdout:write(string.format("started: '%s', PID: %d, numrun: %d\n", appname, newpid, nrun)) io.stdout:flush() if jobn.restart ~= jobn.old_restart then -- restore original restart option to `restart jobn.restart = jobn.old_restart end return true end if type(errmsg) ~= "string" then errmsg = "unknown error" end io.stderr:write(string.format("Error, failed to run '%s': %s\n", appname, errmsg)) io.stderr:flush() return false end local function doapp_enable(appname, enable) local jobn = g_appmap[appname] local tagfile = string.format("%s/%s", LINIT_APPDISABLE, appname) jobn.enabled = enable if enable then posix.unlink(tagfile) else if posix.access(LINIT_APPDISABLE) ~= 0 then -- create LINIT_APPDISABLE directory before writing disable tagfile posix.mkdir(LINIT_APPDISABLE) end local thdl = io.open(tagfile, "wb") if thdl then thdl:write(os.date()) thdl:close(); thdl = nil end end posix.sync() return true end local function app_reload(req, msg) local appn = decode_appname(req, msg) if not appn then return false end doapp_stop(appn) local okay, pid = doapp_start(appn), 0 if okay then pid = g_appmap[appn]["pid"] end g_ucon:reply(req, { ["appname"] = appn, ["error"] = okay and 0 or 3, pid = pid, running = (pid ~= 0) and true or false }) return true end local function app_stop(req, msg) local appn = decode_appname(req, msg) if not appn then return false end doapp_stop(appn) g_ucon:reply(req, { ["appname"] = appn, ["error"] = 0 }) return true end local function app_start(req, msg) local appn = decode_appname(req, msg) if not appn then return false end local okay, pid = doapp_start(appn), 0 if okay then pid = g_appmap[appn]["pid"] end g_ucon:reply(req, { ["appname"] = appn, ["error"] = okay and 0 or 3, pid = pid, running = (pid ~= 0) and true or false }) return true end local function app_status(req, msg) local appn = decode_appname(req, msg) if not appn then return false end local jobn = g_appmap[appn] local pid, running = jobn.pidrun, false if pid and pid > 0 then local exited, eval = invoker.waitpid(pid, true) if exited == nil or exited then if type(eval) ~= "number" then eval = 2021 end io.stderr:write(string.format("Warning, application '%s' has terminated: %d\n", appn, eval)) io.stderr:flush() jobn.pidrun = 0 else running = true end end g_ucon:reply(req, { ["appname"] = appn, ["enabled"] = app_is_enabled(appn), pid = pid, running = running, runnum = jobn.numrun }) return true end local function app_enable(req, msg) local appn = decode_appname(req, msg) if not appn then return false end doapp_enable(appn, true) local enabled = app_is_enabled(appn) g_ucon:reply(req, { ["appname"] = appn, ["enabled"] = enabled, ["error"] = enabled and 0 or 1 }) return true end local function app_disable(req, msg) local appn = decode_appname(req, msg) if not appn then return false end doapp_enable(appn, false) local enabled = app_is_enabled(appn) g_ucon:reply(req, { ["appname"] = appn, ["enabled"] = enabled, ["error"] = enabled and 1 or 0 }) return true end UBUS_FUNCS['initd'] = { ["app_reload"] = { app_reload, { appname = ubus.STRING }, }, ["app_restart"] = { app_reload, { appname = ubus.STRING }, }, ["app_stop"] = { app_stop, { appname = ubus.STRING }, }, ["app_start"] = { app_start, { appname = ubus.STRING }, }, ["app_status"] = { app_status, { appname = ubus.STRING }, }, ["app_enable"] = { app_enable, { appname = ubus.STRING }, }, ["app_disable"] = { app_disable, { appname = ubus.STRING }, }, } local function lnxall_load_apps() -- open configuration file, `LINIT_CONFIG local lcfg = io.open(LINIT_CONFIG, "rb") if not lcfg then io.stderr:write(string.format("Error, failed to open: '%s'\n", LINIT_CONFIG)) io.stderr:flush() return false end -- read the whole file local dcfg = lcfg:read("*a") lcfg:close(); lcfg = nil -- check the return value of `read local typn = type(dcfg) if typn ~= "string" or string.len(dcfg) == 0 then io.stderr:write(string.format("Error, failed to read from '%s'\n", LINIT_CONFIG)) io.stderr:flush() return false end -- decode the content as JSON local okay, jcfg = pcall(cjson.decode, dcfg) typn = type(jcfg) if not okay or typn ~= "table" then io.stderr:write(string.format("Error, failed to decode as JSON from '%s'\n", LINIT_CONFIG)) io.stderr:flush() return false end -- check `applist field jcfg = jcfg["applist"] typn = type(jcfg) if typn ~= "table" or type(jcfg[1]) ~= "table" then io.stderr:write(string.format("Error, no application found in '%s'\n", LINIT_CONFIG)) io.stderr:flush() return false end local idx, appcnt, appmap = 0, 0, {} -- digest the configuration file, -- Using `jcfg directory as the table is not suggested while true do idx = idx + 1 local appi = jcfg[idx] typn = type(appi) if typn ~= "table" then break end -- check the `path field local apath = appi["path"] typn = type(apath) if typn ~= "string" or string.len(appi["path"]) == 0 then io.stderr:write(string.format("Error, path not found at index: %d\n", idx)) io.stderr:flush() else -- Add the processed application to the list local newapp = {} local appname = appi["name"] if type(appname) ~= "string" or #appname == 0 then appname = string.match(apath, "/([^/]+)$") if not appname then appname = apath end end newapp.name = appname newapp.path = apath newapp.restart = appi["restart"] and true or false newapp.old_restart = newapp.restart newapp.nostdio = appi["nostdio"] and true or false -- process arguments local kdx, args, argv = 1, {}, appi["argv"] args[kdx] = apath; kdx = kdx + 1 if type(argv) == "table" and type(argv[1]) == "string" then while true do local newarg = argv[kdx - 1] if type(newarg) ~= "string" then break end args[kdx] = newarg; kdx = kdx + 1 end end newapp.argv = args -- argument array newapp.numrun = 0 -- number of times started newapp.pidrun = 0 -- PID of running process newapp.enabled = true -- application defauft to run if posix.access(string.format("%s/%s", LINIT_APPDISABLE, appname)) == 0 then newapp.enabled = false end appcnt = appcnt + 0x1 appmap[appname] = newapp g_applist[appcnt] = appname io.stdout:write(string.format("App added: '%s'\n", apath)) io.stdout:flush() end end -- check the number of added applications if appcnt == 0 then io.stderr:write("Error, no applications found for LNXALL.\n") io.stderr:flush() return false end g_appmap = appmap return true end local function linit_symlink(srcpath, dest) local rval = true local p0 = invoker.readlink(dest) if p0 ~= srcpath then rval = false posix.rmdir(dest) posix.unlink(dest) if posix.link(srcpath, dest, true) == 0 then rval = true end end if not rval then io.stderr:write(string.format("Error, symlink not found: %s => %s\n", dest, srcpath)) io.stderr:flush() end return rval end -- function to check running processes local function linit_check_process() -- if `initrun is true, we should start all applications unconditionally for _, appn in ipairs(g_applist) do local jobm = g_appmap[appn] local typn = type(jobm) if typn ~= "table" then break end local dorun = false -- should the job be started? local rpid = jobm.pidrun if rpid > 0 then local exited, eval = invoker.waitpid(rpid, true) if exited == nil or exited then jobm.pidrun = 0 if type(eval) ~= "number" then eval = 1 end io.stderr:write(string.format("Process has terminated with %d: '%s', PID: %d\n", eval, jobm.path, rpid)) io.stderr:flush() if jobm.restart then dorun = jobm.enabled end end elseif jobm.numrun == 0 or jobm.restart then dorun = jobm.enabled end if dorun then doapp_start(appn) end end return true end local function timer_func() -- io.stderr:write(string.format("In timer function, uptime: %d\n", invoker.uptime())) -- io.stderr:flush() linit_check_process() local nowt = invoker.uptimsec() g_timestamp = g_timestamp + LINIT_CHKINVAL if g_timestamp <= nowt then g_timestamp = nowt + LINIT_CHKINVAL end if g_timer then g_timer:set(math.floor(g_timestamp - nowt)) end return true end local function update_facini(dest) local okay, output = invoker.invoke(invoker.NOSTDIO + invoker.OUTPUT, "factory", "get") if okay ~= 0 or type(output) ~= "string" then okay = tostring(okay) io.stderr:write("Error, factory get has failed: %s\n", okay or 'unknown') io.stderr:flush() return false end -- update MAC address for WAN if it does not match in `/etc/config/network local macaddr = string.match(output, "MAC=([^\r\n]+)") if macaddr and #macaddr >= 17 then macaddr = macaddr:lower() if invoker.invoke(invoker.NOSTDIO, "uci_set_mac", "wan", macaddr) == 0 then -- important: delay 12 seconds before invoking netuci: invoker.invoke(0, "nothup", "/bin/sh", "-c", "sleep 12 ; exec netuci") end end local sn = string.match(output, "SN=([^\r\n]+)") local hwver = string.match(output, "HWVER=([^\r\n]+)") or '1.0.0.0' local date = string.match(output, "DATE=([^\r\n]+)") or '2018-12-05' if type(sn) ~= "string" or string.len(sn) == 0 then io.stderr:write("Error, factory get has no output.\n") io.stderr:flush() return false end local hsn = invoker.readfile('/etc/hostname', invoker.TRIMEND) if hsn ~= sn then local filh = io.open("/etc/hostname", "wb") if filh then filh:write(sn); filh:write("\n") filh:close(); filh = nil end end local fini = string.format("[factory]\nsoftware_ver = 1.0.0.0\nhardware_ver = %s\ndate = %s\nsn = %s\n", hwver, date, sn) local cont = invoker.readfile(dest) if cont == fini then io.stdout:write(string.format("No need to update %s\n", dest)) io.stdout:flush() return false end local filp = io.open(dest, "wb") if filp then filp:write(fini) filp:close(); filp = nil posix.sync() return true end return false end -- initialization function local function linit_initialize() uloop.init() -- initialize event loop library -- reset signal handlers for SIGINT/SIGCHILD posix.signal(posix.SIGINT, posix.SIG_DFL) posix.signal(posix.SIGCHLD, posix.SIG_DFL) if posix.chdir('/') ~= 0 then io.stderr:write("Erorr, failed to goto root directory\n") io.stderr:flush() return false end -- symlink /opt/lnxall_app/app as /app if not linit_symlink(LINIT_APPDIR, '/app') then return false end -- symlink /opt/lnxall_app/dyiot as /lib/dyiot if not linit_symlink('/opt/lnxall_app/lib/dyiot', '/lib/dyiot') then return false end -- create application disable tag directory posix.mkdir(LINIT_APPDISABLE) -- crown myself as process group leader local okay, errmsg = invoker.setsid() if not okay then if type(errmsg) ~= "string" then errmsg = "Error, setsid has failed." end io.stderr:write(errmsg); io.stderr:write("\n") io.stderr:flush() -- return false end invoker.setname('LNXALL_INIT') -- setup PATH environment variable for all child processes if posix.setenv('PATH', '/opt/lnxall_app/bin:/bin:/sbin:/usr/bin:/usr/sbin', true) ~= 0 then io.stderr:write("Error, failed to setup PATH environment variable.\n") io.stderr:flush() return false end -- update /app/config/fac.ini if necessary if update_facini('/app/config/fac.ini') then io.stdout:write("fac.ini has been updated\n") io.stdout:flush() invoker.invoke(invoker.NOSTDIO, "systemctl", "restart", "ratholec") end -- invoke linit_system.sh to do system-level initializations if invoker.invoke(0, LINIT_SYSTEM) ~= 0 then io.stderr:write(string.format("Error, script has failed: '%s'\n", LINIT_SYSTEM)) io.stderr:flush() return false end -- register signal handler for SIGINT posix.signal(posix.SIGINT, sigint_handler) posix.signal(posix.SIGCHLD, sigint_handler) -- load all applications from `lnxall_applist.json if not lnxall_load_apps() then return false end -- this will start ubusd service linit_check_process() while true do -- delay at least 1.5 seconds, -- to wait for ubusd initialization if invoker.msleep(1500) == 0 then break end end -- create timer handle g_timestamp = invoker.uptimsec() g_timer = uloop.timer(timer_func) if not g_timer then io.stderr:write("Error, failed to create timer handle!\n") io.stderr:flush() return false end -- connect to ubus daemon g_ucon = ubus.connect() if not g_ucon then io.stderr:write("Error, failed to connect to ubus daemon!\n") io.stderr:flush() return false end g_ucon:add(UBUS_FUNCS) g_timer:set(5000) -- start timer in 5 seconds return true end local function linit_loop() while g_looping do if g_sigchild then linit_check_process() g_sigchild = false end uloop.run(2000) end -- emit a warning message io.stderr:write(string.format("Error, linit process will terminate: %s\n", tostring(g_looping))) io.stderr:flush() -- terminate all child processes: invoker.killpg(0, posix.SIGKILL) end if linit_initialize() then linit_loop() -- application hangs here end os.exit(2)