#!/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/upstart/lnxall_applist.json' local LINIT_CONDIR = '/app/upstart/applist' local LINIT_SYSTEM = '/opt/lnxall_app/platspec/linit_system.sh' local LINIT_CHKINVAL = 30000 -- every 30 seconds local gfmt = string.format -- 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 badstr(s) if type(s) ~= "string" then return true end if string.len(s) == 0 then return true end return false end 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(gfmt("Error, unknown signal received: %d\n", signo)) io.stderr:flush() end end local function decode_appname(reqmsg, ubusmsg) local appname = nil if type(ubusmsg) == "table" then appname = ubusmsg["appname"] end if badstr(appname) 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 = gfmt("appname not found: '%s'", appname) }) return nil end return appname end local function app_is_enabled(appname) local tagfile = gfmt("%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(gfmt("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(gfmt("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(gfmt("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 = gfmt("%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(gfmt("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 local function load_appinfo(vartype) local okay, appinfo = true, nil if type(vartype) == "string" then local fdat = invoker.readfile(vartype, 0x100000) if not fdat or #fdat == 0 then return nil, gfmt("filepath not found: %s", vartype) end okay, appinfo = pcall(cjson.decode, fdat) if not okay or type(appinfo) ~= "table" then return nil, gfmt("cannot decode as JSON: %s", vartype) end elseif type(vartype) == "table" then appinfo = vartype else return nil, gfmt("unknown type for load_appinfo: %s", type(vartype)) end local name, apath = appinfo.name, appinfo.path if badstr(name) or badstr(apath) then if type(vartype) == "string" then return nil, gfmt("invalid name or path found: %s", vartype) end return nil, "invalid name or path found" end local oldapp, isnew = g_appmap[name], false if type(oldapp) ~= "table" then oldapp, isnew = {}, true end if oldapp.name ~= name then oldapp.name = name end -- update name of application if oldapp.path ~= apath then oldapp.path = apath end -- update executable path for application oldapp.restart = appinfo.restart and true or false oldapp.old_restart = oldapp.restart oldapp.nostdio = appinfo.nostdio and true or false -- process arguments one-by-one local args, argv = { [1] = apath, }, appinfo["argv"] if type(argv) == "table" and type(argv[1]) == "string" then local idx = 1 while true do local newarg = argv[idx] if type(newarg) ~= "string" then break end idx = idx + 1 args[idx] = newarg end end oldapp.argv = args -- update argument array unconditionally if isnew then oldapp.numrun = 0 -- number of times started oldapp.pidrun = 0 -- PID of running process oldapp.enabled = true -- application defauft to run if posix.access(gfmt("%s/%s", LINIT_APPDISABLE, name)) == 0 then oldapp.enabled = false end local lnum = #g_applist g_applist[lnum + 1] = name end g_appmap[name] = oldapp return name end local function decode_appfile(reqmsg, ubusmsg) local fpath, aname = nil, nil if type(ubusmsg) == "table" then fpath, aname = ubusmsg["filepath"], ubusmsg["appname"] end if badstr(fpath) or badstr(aname) then g_ucon:reply(reqmsg, { ["appname"] = aname, ["error"] = 1, errmsg = "filepath not specified." }) return nil end local name, errmsg = load_appinfo(fpath) if not name then g_ucon:reply(reqmsg, { ["appname"] = aname, ["error"] = 2, ["errmsg"] = errmsg }) return nil end return name end local function app_addfile(req, msg) local name = decode_appfile(req, msg) if name then g_ucon:reply(req, { ["appname"] = name, ["error"] = 0, ["status"] = gfmt("Application updated: %s", name) }) end return true end local function app_delfile(req, msg) local appn = decode_appfile(req, msg) if badstr(appn) then return true end local realidx = nil for idx, name in ipairs(g_applist) do if name == appn then realidx = idx break end end if realidx then doapp_stop(appn) doapp_enable(appn, false) g_appmap[appn] = nil table.remove(g_applist, realidx) g_ucon:reply(req, { ["appname"] = appn, ["error"] = 0, ["errmsg"] = gfmt("Application removed: %s", appn) }) io.stdout:write(gfmt("INFO, OLDAPP removed: %s\n", appn)) io.stdout:flush() else g_ucon:reply(req, { ["appname"] = appn, ["error"] = 3, ["errmsg"] = gfmt("failed to remove application: %s", appn) }) io.stderr:write(gfmt("Error, OLDAPP remove failed: %s\n", appn)) io.stderr:flush() end 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 }, }, ["app_addfile"] = { app_addfile, { appname = ubus.STRING, filepath = ubus.STRING }, }, ["app_delfile"] = { app_delfile, { appname = ubus.STRING, filepath = 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(gfmt("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 if badstr(dcfg) then io.stderr:write(gfmt("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) if not okay or type(jcfg) ~= "table" then io.stderr:write(gfmt("Error, failed to decode as JSON from '%s'\n", LINIT_CONFIG)) io.stderr:flush() return false end -- check `applist field jcfg = jcfg["applist"] if type(jcfg) ~= "table" or type(jcfg[1]) ~= "table" then io.stderr:write(gfmt("Error, no application found in '%s'\n", LINIT_CONFIG)) io.stderr:flush() return false end local idx, appcnt = 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] if type(appi) ~= "table" then break end -- check the `path field local apath = appi["path"] if badstr(apath) then io.stderr:write(gfmt("Error, path not found at index: %d\n", idx)) io.stderr:flush() else -- Add the processed application to the list local name = load_appinfo(appi) if name then io.stdout:write(gfmt("New App added, name: %s => '%s'\n", name, apath)) io.stdout:flush() end end end -- load and update standalone applications local aplist = posix.glob(LINIT_CONDIR .. "/*.json") if type(aplist) == "table" then if #aplist > 1 then table.sort(aplist) end for _, apnext in ipairs(aplist) do local name = load_appinfo(apnext) if name then io.stdout:write(gfmt("New App added or updated, name: %s\n", name)) io.stdout:flush() end end end -- check the number of added applications if #g_applist == 0 then io.stderr:write("Error, no applications found for LNXALL.\n") io.stderr:flush() return false end 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(gfmt("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] if type(jobm) ~= "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(gfmt("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(gfmt("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 badstr(sn) 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 = gfmt("[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(gfmt("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 local function call_system_init() local sysinit = "/app/upstart/000-system_init.sh" if not invoker.statfile(sysinit) then return false end local systag = "/app/upstart/.000-system_init.sh.done" if invoker.statfile(systag) then -- Already called previously return true end local shell = "/bin/bash" if not invoker.statfile(shell) then shell = "/bin.sh" end invoker.invoke(0, shell, sysinit) local tagh = io.open(systag, "wb") if tagh then tagh:write(gfmt("%s: system_init.sh called by LINIT.\n", os.date())) tagh:close() end return true 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:/usr/local/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(gfmt("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 call_system_init() -- 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() -- initialize/install other services: invoker.invoke(0, "nothup", "/bin/sh", "-c", "sleep 3 ; exec lua /opt/lnxall_app/bin/lupstart.lua") 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(gfmt("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)