| 1 | --[[ |
|---|
| 2 | LuCI - Lua Development Framework |
|---|
| 3 | |
|---|
| 4 | Copyright 2009 Steven Barth <steven@midlink.org> |
|---|
| 5 | |
|---|
| 6 | Licensed under the Apache License, Version 2.0 (the "License"); |
|---|
| 7 | you may not use this file except in compliance with the License. |
|---|
| 8 | You may obtain a copy of the License at |
|---|
| 9 | |
|---|
| 10 | http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 11 | |
|---|
| 12 | $Id$ |
|---|
| 13 | ]] |
|---|
| 14 | |
|---|
| 15 | local nixio = require "nixio" |
|---|
| 16 | local table = require "table" |
|---|
| 17 | local uci = require "luci.model.uci" |
|---|
| 18 | local os = require "os" |
|---|
| 19 | local io = require "io" |
|---|
| 20 | |
|---|
| 21 | local pairs, require, pcall, assert, type = pairs, require, pcall, assert, type |
|---|
| 22 | local ipairs, tonumber, collectgarbage = ipairs, tonumber, collectgarbage |
|---|
| 23 | |
|---|
| 24 | |
|---|
| 25 | module "luci.lucid" |
|---|
| 26 | |
|---|
| 27 | local slaves = {} |
|---|
| 28 | local pollt = {} |
|---|
| 29 | local tickt = {} |
|---|
| 30 | local tpids = {} |
|---|
| 31 | local tcount = 0 |
|---|
| 32 | local ifaddrs = nixio.getifaddrs() |
|---|
| 33 | |
|---|
| 34 | cursor = uci.cursor() |
|---|
| 35 | state = uci.cursor_state() |
|---|
| 36 | UCINAME = "lucid" |
|---|
| 37 | |
|---|
| 38 | local cursor = cursor |
|---|
| 39 | local state = state |
|---|
| 40 | local UCINAME = UCINAME |
|---|
| 41 | local SSTATE = "/tmp/.lucid_store" |
|---|
| 42 | |
|---|
| 43 | |
|---|
| 44 | --- Starts a new LuCId superprocess. |
|---|
| 45 | function start() |
|---|
| 46 | state:revert(UCINAME, "main") |
|---|
| 47 | |
|---|
| 48 | prepare() |
|---|
| 49 | |
|---|
| 50 | local detach = cursor:get(UCINAME, "main", "daemonize") |
|---|
| 51 | if detach == "1" then |
|---|
| 52 | local stat, code, msg = daemonize() |
|---|
| 53 | if not stat then |
|---|
| 54 | nixio.syslog("crit", "Unable to detach process: " .. msg .. "\n") |
|---|
| 55 | ox.exit(2) |
|---|
| 56 | end |
|---|
| 57 | end |
|---|
| 58 | |
|---|
| 59 | state:set(UCINAME, "main", "pid", nixio.getpid()) |
|---|
| 60 | state:save(UCINAME) |
|---|
| 61 | |
|---|
| 62 | run() |
|---|
| 63 | end |
|---|
| 64 | |
|---|
| 65 | --- Returns the PID of the currently active LuCId process. |
|---|
| 66 | function running() |
|---|
| 67 | local pid = tonumber(state:get(UCINAME, "main", "pid")) |
|---|
| 68 | return pid and nixio.kill(pid, 0) and pid |
|---|
| 69 | end |
|---|
| 70 | |
|---|
| 71 | --- Stops any running LuCId superprocess. |
|---|
| 72 | function stop() |
|---|
| 73 | local pid = tonumber(state:get(UCINAME, "main", "pid")) |
|---|
| 74 | if pid then |
|---|
| 75 | return nixio.kill(pid, nixio.const.SIGTERM) |
|---|
| 76 | end |
|---|
| 77 | return false |
|---|
| 78 | end |
|---|
| 79 | |
|---|
| 80 | --- Prepares the slaves, daemons and publishers, allocate resources. |
|---|
| 81 | function prepare() |
|---|
| 82 | local debug = tonumber((cursor:get(UCINAME, "main", "debug"))) |
|---|
| 83 | |
|---|
| 84 | nixio.openlog("lucid", "pid", "perror") |
|---|
| 85 | if debug ~= 1 then |
|---|
| 86 | nixio.setlogmask("warning") |
|---|
| 87 | end |
|---|
| 88 | |
|---|
| 89 | cursor:foreach(UCINAME, "daemon", function(config) |
|---|
| 90 | if config.enabled ~= "1" then |
|---|
| 91 | return |
|---|
| 92 | end |
|---|
| 93 | |
|---|
| 94 | local key = config[".name"] |
|---|
| 95 | if not config.slave then |
|---|
| 96 | nixio.syslog("crit", "Daemon "..key.." is missing a slave\n") |
|---|
| 97 | os.exit(1) |
|---|
| 98 | else |
|---|
| 99 | nixio.syslog("info", "Initializing daemon " .. key) |
|---|
| 100 | end |
|---|
| 101 | |
|---|
| 102 | state:revert(UCINAME, key) |
|---|
| 103 | |
|---|
| 104 | local daemon, code, err = prepare_daemon(config) |
|---|
| 105 | if daemon then |
|---|
| 106 | state:set(UCINAME, key, "status", "started") |
|---|
| 107 | nixio.syslog("info", "Prepared daemon " .. key) |
|---|
| 108 | else |
|---|
| 109 | state:set(UCINAME, key, "status", "error") |
|---|
| 110 | state:set(UCINAME, key, "error", err) |
|---|
| 111 | nixio.syslog("err", "Failed to initialize daemon "..key..": ".. |
|---|
| 112 | err .. "\n") |
|---|
| 113 | end |
|---|
| 114 | end) |
|---|
| 115 | end |
|---|
| 116 | |
|---|
| 117 | --- Run the superprocess if prepared before. |
|---|
| 118 | -- This main function of LuCId will wait for events on given file descriptors. |
|---|
| 119 | function run() |
|---|
| 120 | local pollint = tonumber((cursor:get(UCINAME, "main", "pollinterval"))) |
|---|
| 121 | local threadlimit = tonumber((cursor:get(UCINAME, "main", "threadlimit"))) |
|---|
| 122 | |
|---|
| 123 | while true do |
|---|
| 124 | local stat, code = nixio.poll(pollt, pollint) |
|---|
| 125 | |
|---|
| 126 | if stat and stat > 0 then |
|---|
| 127 | local ok = false |
|---|
| 128 | for _, polle in ipairs(pollt) do |
|---|
| 129 | if polle.revents ~= 0 and polle.handler then |
|---|
| 130 | ok = ok or polle.handler(polle) |
|---|
| 131 | end |
|---|
| 132 | end |
|---|
| 133 | if not ok then |
|---|
| 134 | -- Avoid high CPU usage if thread limit is reached |
|---|
| 135 | nixio.nanosleep(0, 100000000) |
|---|
| 136 | end |
|---|
| 137 | elseif stat == 0 then |
|---|
| 138 | ifaddrs = nixio.getifaddrs() |
|---|
| 139 | end |
|---|
| 140 | |
|---|
| 141 | for _, cb in ipairs(tickt) do |
|---|
| 142 | cb() |
|---|
| 143 | end |
|---|
| 144 | |
|---|
| 145 | local pid, stat, code = nixio.wait(-1, "nohang") |
|---|
| 146 | while pid and pid > 0 do |
|---|
| 147 | nixio.syslog("info", "Buried thread: " .. pid) |
|---|
| 148 | if tpids[pid] then |
|---|
| 149 | tcount = tcount - 1 |
|---|
| 150 | if tpids[pid] ~= true then |
|---|
| 151 | tpids[pid](pid, stat, code) |
|---|
| 152 | end |
|---|
| 153 | end |
|---|
| 154 | pid, stat, code = nixio.wait(-1, "nohang") |
|---|
| 155 | end |
|---|
| 156 | end |
|---|
| 157 | end |
|---|
| 158 | |
|---|
| 159 | --- Add a file descriptor for the main loop and associate handler functions. |
|---|
| 160 | -- @param polle Table containing: {fd = FILE DESCRIPTOR, events = POLL EVENTS, |
|---|
| 161 | -- handler = EVENT HANDLER CALLBACK} |
|---|
| 162 | -- @see unregister_pollfd |
|---|
| 163 | -- @return boolean status |
|---|
| 164 | function register_pollfd(polle) |
|---|
| 165 | pollt[#pollt+1] = polle |
|---|
| 166 | return true |
|---|
| 167 | end |
|---|
| 168 | |
|---|
| 169 | --- Unregister a file desciptor and associate handler from the main loop. |
|---|
| 170 | -- @param polle Poll descriptor |
|---|
| 171 | -- @see register_pollfd |
|---|
| 172 | -- @return boolean status |
|---|
| 173 | function unregister_pollfd(polle) |
|---|
| 174 | for k, v in ipairs(pollt) do |
|---|
| 175 | if v == polle then |
|---|
| 176 | table.remove(pollt, k) |
|---|
| 177 | return true |
|---|
| 178 | end |
|---|
| 179 | end |
|---|
| 180 | return false |
|---|
| 181 | end |
|---|
| 182 | |
|---|
| 183 | --- Close all registered file descriptors from main loop. |
|---|
| 184 | -- This is useful for forked child processes. |
|---|
| 185 | function close_pollfds() |
|---|
| 186 | for k, v in ipairs(pollt) do |
|---|
| 187 | if v.fd and v.fd.close then |
|---|
| 188 | v.fd:close() |
|---|
| 189 | end |
|---|
| 190 | end |
|---|
| 191 | end |
|---|
| 192 | |
|---|
| 193 | --- Register a tick function that will be called at each cycle of the main loop. |
|---|
| 194 | -- @param cb Callback |
|---|
| 195 | -- @see unregister_tick |
|---|
| 196 | -- @return boolean status |
|---|
| 197 | function register_tick(cb) |
|---|
| 198 | tickt[#tickt+1] = cb |
|---|
| 199 | return true |
|---|
| 200 | end |
|---|
| 201 | |
|---|
| 202 | --- Unregister a tick function from the main loop. |
|---|
| 203 | -- @param cb Callback |
|---|
| 204 | -- @see register_tick |
|---|
| 205 | -- @return boolean status |
|---|
| 206 | function unregister_tick(cb) |
|---|
| 207 | for k, v in ipairs(tickt) do |
|---|
| 208 | if v == cb then |
|---|
| 209 | table.remove(tickt, k) |
|---|
| 210 | return true |
|---|
| 211 | end |
|---|
| 212 | end |
|---|
| 213 | return false |
|---|
| 214 | end |
|---|
| 215 | |
|---|
| 216 | --- Tests whether a given number of processes can be created. |
|---|
| 217 | -- @oaram num Processes to be created |
|---|
| 218 | -- @return boolean status |
|---|
| 219 | function try_process(num) |
|---|
| 220 | local threadlimit = tonumber((cursor:get(UCINAME, "main", "threadlimit"))) |
|---|
| 221 | return not threadlimit or (threadlimit - tcount) >= (num or 1) |
|---|
| 222 | end |
|---|
| 223 | |
|---|
| 224 | --- Create a new child process from a Lua function and assign a destructor. |
|---|
| 225 | -- @param threadcb main function of the new process |
|---|
| 226 | -- @param waitcb destructor callback |
|---|
| 227 | -- @return process identifier or nil, error code, error message |
|---|
| 228 | function create_process(threadcb, waitcb) |
|---|
| 229 | local threadlimit = tonumber(cursor:get(UCINAME, "main", "threadlimit")) |
|---|
| 230 | if threadlimit and tcount >= threadlimit then |
|---|
| 231 | nixio.syslog("warning", "Cannot create thread: process limit reached") |
|---|
| 232 | return nil |
|---|
| 233 | else |
|---|
| 234 | collectgarbage("collect") |
|---|
| 235 | end |
|---|
| 236 | local pid, code, err = nixio.fork() |
|---|
| 237 | if pid and pid ~= 0 then |
|---|
| 238 | nixio.syslog("info", "Created thread: " .. pid) |
|---|
| 239 | tpids[pid] = waitcb or true |
|---|
| 240 | tcount = tcount + 1 |
|---|
| 241 | elseif pid == 0 then |
|---|
| 242 | local code = threadcb() |
|---|
| 243 | os.exit(code) |
|---|
| 244 | else |
|---|
| 245 | nixio.syslog("err", "Unable to fork(): " .. err) |
|---|
| 246 | end |
|---|
| 247 | return pid, code, err |
|---|
| 248 | end |
|---|
| 249 | |
|---|
| 250 | --- Prepare a daemon from a given configuration table. |
|---|
| 251 | -- @param config Configuration data. |
|---|
| 252 | -- @return boolean status or nil, error code, error message |
|---|
| 253 | function prepare_daemon(config) |
|---|
| 254 | nixio.syslog("info", "Preparing daemon " .. config[".name"]) |
|---|
| 255 | local modname = cursor:get(UCINAME, config.slave) |
|---|
| 256 | if not modname then |
|---|
| 257 | return nil, -1, "invalid slave" |
|---|
| 258 | end |
|---|
| 259 | |
|---|
| 260 | local stat, module = pcall(require, _NAME .. "." .. modname) |
|---|
| 261 | if not stat or not module.prepare_daemon then |
|---|
| 262 | return nil, -2, "slave type not supported" |
|---|
| 263 | end |
|---|
| 264 | |
|---|
| 265 | config.slave = prepare_slave(config.slave) |
|---|
| 266 | |
|---|
| 267 | return module.prepare_daemon(config, _M) |
|---|
| 268 | end |
|---|
| 269 | |
|---|
| 270 | --- Prepare a slave. |
|---|
| 271 | -- @param name slave name |
|---|
| 272 | -- @return table containing slave module and configuration or nil, error message |
|---|
| 273 | function prepare_slave(name) |
|---|
| 274 | local slave = slaves[name] |
|---|
| 275 | if not slave then |
|---|
| 276 | local config = cursor:get_all(UCINAME, name) |
|---|
| 277 | |
|---|
| 278 | local stat, module = pcall(require, config and config.entrypoint) |
|---|
| 279 | if stat then |
|---|
| 280 | slave = {module = module, config = config} |
|---|
| 281 | end |
|---|
| 282 | end |
|---|
| 283 | |
|---|
| 284 | if slave then |
|---|
| 285 | return slave |
|---|
| 286 | else |
|---|
| 287 | return nil, module |
|---|
| 288 | end |
|---|
| 289 | end |
|---|
| 290 | |
|---|
| 291 | --- Return a list of available network interfaces on the host. |
|---|
| 292 | -- @return table returned by nixio.getifaddrs() |
|---|
| 293 | function get_interfaces() |
|---|
| 294 | return ifaddrs |
|---|
| 295 | end |
|---|
| 296 | |
|---|
| 297 | --- Revoke process privileges. |
|---|
| 298 | -- @param user new user name or uid |
|---|
| 299 | -- @param group new group name or gid |
|---|
| 300 | -- @return boolean status or nil, error code, error message |
|---|
| 301 | function revoke_privileges(user, group) |
|---|
| 302 | if nixio.getuid() == 0 then |
|---|
| 303 | return nixio.setgid(group) and nixio.setuid(user) |
|---|
| 304 | end |
|---|
| 305 | end |
|---|
| 306 | |
|---|
| 307 | --- Return a secure UCI cursor. |
|---|
| 308 | -- @return UCI cursor |
|---|
| 309 | function securestate() |
|---|
| 310 | local stat = nixio.fs.stat(SSTATE) or {} |
|---|
| 311 | local uid = nixio.getuid() |
|---|
| 312 | if stat.type ~= "dir" or (stat.modedec % 100) ~= 0 or stat.uid ~= uid then |
|---|
| 313 | nixio.fs.remover(SSTATE) |
|---|
| 314 | if not nixio.fs.mkdir(SSTATE, 700) then |
|---|
| 315 | local errno = nixio.errno() |
|---|
| 316 | nixio.syslog("err", "Integrity check on secure state failed!") |
|---|
| 317 | return nil, errno, nixio.perror(errno) |
|---|
| 318 | end |
|---|
| 319 | end |
|---|
| 320 | |
|---|
| 321 | return uci.cursor(nil, SSTATE) |
|---|
| 322 | end |
|---|
| 323 | |
|---|
| 324 | --- Daemonize the process. |
|---|
| 325 | -- @return boolean status or nil, error code, error message |
|---|
| 326 | function daemonize() |
|---|
| 327 | if nixio.getppid() == 1 then |
|---|
| 328 | return |
|---|
| 329 | end |
|---|
| 330 | |
|---|
| 331 | local pid, code, msg = nixio.fork() |
|---|
| 332 | if not pid then |
|---|
| 333 | return nil, code, msg |
|---|
| 334 | elseif pid > 0 then |
|---|
| 335 | os.exit(0) |
|---|
| 336 | end |
|---|
| 337 | |
|---|
| 338 | nixio.setsid() |
|---|
| 339 | nixio.chdir("/") |
|---|
| 340 | |
|---|
| 341 | local devnull = nixio.open("/dev/null", nixio.open_flags("rdwr")) |
|---|
| 342 | nixio.dup(devnull, nixio.stdin) |
|---|
| 343 | nixio.dup(devnull, nixio.stdout) |
|---|
| 344 | nixio.dup(devnull, nixio.stderr) |
|---|
| 345 | |
|---|
| 346 | return true |
|---|
| 347 | end |
|---|