341 lines
8.4 KiB
Lua
341 lines
8.4 KiB
Lua
--[[
|
|
LuCI - Lua Development Framework
|
|
|
|
Copyright 2009 Steven Barth <steven@midlink.org>
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
$Id$
|
|
]]
|
|
|
|
local nixio = require "nixio"
|
|
local table = require "table"
|
|
local uci = require "luci.model.uci"
|
|
local os = require "os"
|
|
local io = require "io"
|
|
|
|
local pairs, require, pcall, assert, type = pairs, require, pcall, assert, type
|
|
local ipairs, tonumber, collectgarbage = ipairs, tonumber, collectgarbage
|
|
|
|
|
|
module "luci.lucid"
|
|
|
|
local slaves = {}
|
|
local pollt = {}
|
|
local tickt = {}
|
|
local tpids = {}
|
|
local tcount = 0
|
|
local ifaddrs = nixio.getifaddrs()
|
|
|
|
cursor = uci.cursor()
|
|
state = uci.cursor_state()
|
|
UCINAME = "lucid"
|
|
|
|
local cursor = cursor
|
|
local state = state
|
|
local UCINAME = UCINAME
|
|
local SSTATE = "/tmp/.lucid_store"
|
|
|
|
|
|
--- Starts a new LuCId superprocess.
|
|
function start()
|
|
prepare()
|
|
|
|
local detach = cursor:get(UCINAME, "main", "daemonize")
|
|
if detach == "1" then
|
|
local stat, code, msg = daemonize()
|
|
if not stat then
|
|
nixio.syslog("crit", "Unable to detach process: " .. msg .. "\n")
|
|
ox.exit(2)
|
|
end
|
|
end
|
|
|
|
state:set(UCINAME, "main", "pid", nixio.getpid())
|
|
state:save(UCINAME)
|
|
|
|
run()
|
|
end
|
|
|
|
--- Returns the PID of the currently active LuCId process.
|
|
function running()
|
|
local pid = tonumber(state:get(UCINAME, "main", "pid"))
|
|
return pid and nixio.kill(pid, 0) and pid
|
|
end
|
|
|
|
--- Stops any running LuCId superprocess.
|
|
function stop()
|
|
local pid = tonumber(state:get(UCINAME, "main", "pid"))
|
|
if pid then
|
|
return nixio.kill(pid, nixio.const.SIGTERM)
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Prepares the slaves, daemons and publishers, allocate resources.
|
|
function prepare()
|
|
local debug = tonumber((cursor:get(UCINAME, "main", "debug")))
|
|
|
|
nixio.openlog("lucid", "pid", "perror")
|
|
if debug ~= 1 then
|
|
nixio.setlogmask("warning")
|
|
end
|
|
|
|
cursor:foreach(UCINAME, "daemon", function(config)
|
|
if config.enabled ~= "1" then
|
|
return
|
|
end
|
|
|
|
local key = config[".name"]
|
|
if not config.slave then
|
|
nixio.syslog("crit", "Daemon "..key.." is missing a slave\n")
|
|
os.exit(1)
|
|
else
|
|
nixio.syslog("info", "Initializing daemon " .. key)
|
|
end
|
|
|
|
state:revert(UCINAME, key)
|
|
|
|
local daemon, code, err = prepare_daemon(config)
|
|
if daemon then
|
|
state:set(UCINAME, key, "status", "started")
|
|
nixio.syslog("info", "Prepared daemon " .. key)
|
|
else
|
|
state:set(UCINAME, key, "status", "error")
|
|
state:set(UCINAME, key, "error", err)
|
|
nixio.syslog("err", "Failed to initialize daemon "..key..": "..
|
|
err .. "\n")
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Run the superprocess if prepared before.
|
|
-- This main function of LuCId will wait for events on given file descriptors.
|
|
function run()
|
|
local pollint = tonumber((cursor:get(UCINAME, "main", "pollinterval")))
|
|
local threadlimit = tonumber((cursor:get(UCINAME, "main", "threadlimit")))
|
|
|
|
while true do
|
|
local stat, code = nixio.poll(pollt, pollint)
|
|
|
|
if stat and stat > 0 then
|
|
local ok = false
|
|
for _, polle in ipairs(pollt) do
|
|
if polle.revents ~= 0 and polle.handler then
|
|
ok = ok or polle.handler(polle)
|
|
end
|
|
end
|
|
if not ok then
|
|
-- Avoid high CPU usage if thread limit is reached
|
|
nixio.nanosleep(0, 100000000)
|
|
end
|
|
elseif stat == 0 then
|
|
ifaddrs = nixio.getifaddrs()
|
|
collectgarbage("collect")
|
|
end
|
|
|
|
for _, cb in ipairs(tickt) do
|
|
cb()
|
|
end
|
|
|
|
local pid, stat, code = nixio.wait(-1, "nohang")
|
|
while pid and pid > 0 do
|
|
tcount = tcount - 1
|
|
if tpids[pid] and tpids[pid] ~= true then
|
|
tpids[pid](pid, stat, code)
|
|
end
|
|
pid, stat, code = nixio.wait(-1, "nohang")
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Add a file descriptor for the main loop and associate handler functions.
|
|
-- @param polle Table containing: {fd = FILE DESCRIPTOR, events = POLL EVENTS,
|
|
-- handler = EVENT HANDLER CALLBACK}
|
|
-- @see unregister_pollfd
|
|
-- @return boolean status
|
|
function register_pollfd(polle)
|
|
pollt[#pollt+1] = polle
|
|
return true
|
|
end
|
|
|
|
--- Unregister a file desciptor and associate handler from the main loop.
|
|
-- @param polle Poll descriptor
|
|
-- @see register_pollfd
|
|
-- @return boolean status
|
|
function unregister_pollfd(polle)
|
|
for k, v in ipairs(pollt) do
|
|
if v == polle then
|
|
table.remove(pollt, k)
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Close all registered file descriptors from main loop.
|
|
-- This is useful for forked child processes.
|
|
function close_pollfds()
|
|
for k, v in ipairs(pollt) do
|
|
if v.fd and v.fd.close then
|
|
v.fd:close()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Register a tick function that will be called at each cycle of the main loop.
|
|
-- @param cb Callback
|
|
-- @see unregister_tick
|
|
-- @return boolean status
|
|
function register_tick(cb)
|
|
tickt[#tickt+1] = cb
|
|
return true
|
|
end
|
|
|
|
--- Unregister a tick function from the main loop.
|
|
-- @param cb Callback
|
|
-- @see register_tick
|
|
-- @return boolean status
|
|
function unregister_tick(cb)
|
|
for k, v in ipairs(tickt) do
|
|
if v == cb then
|
|
table.remove(tickt, k)
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Tests whether a given number of processes can be created.
|
|
-- @oaram num Processes to be created
|
|
-- @return boolean status
|
|
function try_process(num)
|
|
local threadlimit = tonumber((cursor:get(UCINAME, "main", "threadlimit")))
|
|
return not threadlimit or (threadlimit - tcount) >= (num or 1)
|
|
end
|
|
|
|
--- Create a new child process from a Lua function and assign a destructor.
|
|
-- @param threadcb main function of the new process
|
|
-- @param waitcb destructor callback
|
|
-- @return process identifier or nil, error code, error message
|
|
function create_process(threadcb, waitcb)
|
|
local threadlimit = tonumber(cursor:get(UCINAME, "main", "threadlimit"))
|
|
if threadlimit and tcount >= threadlimit then
|
|
nixio.syslog("warning", "Cannot create thread: process limit reached")
|
|
return nil
|
|
end
|
|
local pid, code, err = nixio.fork()
|
|
if pid and pid ~= 0 then
|
|
tpids[pid] = waitcb
|
|
tcount = tcount + 1
|
|
elseif pid == 0 then
|
|
local code = threadcb()
|
|
os.exit(code)
|
|
else
|
|
nixio.syslog("err", "Unable to fork(): " .. err)
|
|
end
|
|
return pid, code, err
|
|
end
|
|
|
|
--- Prepare a daemon from a given configuration table.
|
|
-- @param config Configuration data.
|
|
-- @return boolean status or nil, error code, error message
|
|
function prepare_daemon(config)
|
|
nixio.syslog("info", "Preparing daemon " .. config[".name"])
|
|
local modname = cursor:get(UCINAME, config.slave)
|
|
if not modname then
|
|
return nil, -1, "invalid slave"
|
|
end
|
|
|
|
local stat, module = pcall(require, _NAME .. "." .. modname)
|
|
if not stat or not module.prepare_daemon then
|
|
return nil, -2, "slave type not supported"
|
|
end
|
|
|
|
config.slave = prepare_slave(config.slave)
|
|
|
|
return module.prepare_daemon(config, _M)
|
|
end
|
|
|
|
--- Prepare a slave.
|
|
-- @param name slave name
|
|
-- @return table containing slave module and configuration or nil, error message
|
|
function prepare_slave(name)
|
|
local slave = slaves[name]
|
|
if not slave then
|
|
local config = cursor:get_all(UCINAME, name)
|
|
|
|
local stat, module = pcall(require, config and config.entrypoint)
|
|
if stat then
|
|
slave = {module = module, config = config}
|
|
end
|
|
end
|
|
|
|
if slave then
|
|
return slave
|
|
else
|
|
return nil, module
|
|
end
|
|
end
|
|
|
|
--- Return a list of available network interfaces on the host.
|
|
-- @return table returned by nixio.getifaddrs()
|
|
function get_interfaces()
|
|
return ifaddrs
|
|
end
|
|
|
|
--- Revoke process privileges.
|
|
-- @param user new user name or uid
|
|
-- @param group new group name or gid
|
|
-- @return boolean status or nil, error code, error message
|
|
function revoke_privileges(user, group)
|
|
if nixio.getuid() == 0 then
|
|
return nixio.setgid(group) and nixio.setuid(user)
|
|
end
|
|
end
|
|
|
|
--- Return a secure UCI cursor.
|
|
-- @return UCI cursor
|
|
function securestate()
|
|
local stat = nixio.fs.stat(SSTATE) or {}
|
|
local uid = nixio.getuid()
|
|
if stat.type ~= "dir" or (stat.modedec % 100) ~= 0 or stat.uid ~= uid then
|
|
nixio.fs.remover(SSTATE)
|
|
if not nixio.fs.mkdir(SSTATE, 700) then
|
|
local errno = nixio.errno()
|
|
nixio.syslog("err", "Integrity check on secure state failed!")
|
|
return nil, errno, nixio.perror(errno)
|
|
end
|
|
end
|
|
|
|
return uci.cursor(nil, SSTATE)
|
|
end
|
|
|
|
--- Daemonize the process.
|
|
-- @return boolean status or nil, error code, error message
|
|
function daemonize()
|
|
if nixio.getppid() == 1 then
|
|
return
|
|
end
|
|
|
|
local pid, code, msg = nixio.fork()
|
|
if not pid then
|
|
return nil, code, msg
|
|
elseif pid > 0 then
|
|
os.exit(0)
|
|
end
|
|
|
|
nixio.setsid()
|
|
nixio.chdir("/")
|
|
|
|
local devnull = nixio.open("/dev/null", nixio.open_flags("rdwr"))
|
|
nixio.dup(devnull, nixio.stdin)
|
|
nixio.dup(devnull, nixio.stdout)
|
|
nixio.dup(devnull, nixio.stderr)
|
|
|
|
return true
|
|
end
|