libs/lucid: only execute handlers if daemon is below the thread limit, this allows...
[project/luci.git] / libs / lucid / luasrc / lucid.lua
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         prepare()
47
48         local detach = cursor:get(UCINAME, "main", "daemonize")
49         if detach == "1" then
50                 local stat, code, msg = daemonize()
51                 if not stat then
52                         nixio.syslog("crit", "Unable to detach process: " .. msg .. "\n")
53                         ox.exit(2)
54                 end
55         end
56
57         state:set(UCINAME, "main", "pid", nixio.getpid())
58         state:save(UCINAME)
59
60         run()
61 end
62
63 --- Stops any running LuCId superprocess. 
64 function stop()
65         local pid = tonumber(state:get(UCINAME, "main", "pid"))
66         if pid then
67                 return nixio.kill(pid, nixio.const.SIGTERM)
68         end
69         return false
70 end
71
72 --- Prepares the slaves, daemons and publishers, allocate resources.
73 function prepare()
74         local debug = tonumber((cursor:get(UCINAME, "main", "debug")))
75         
76         nixio.openlog("lucid", "pid", "perror")
77         if debug ~= 1 then
78                 nixio.setlogmask("warning")
79         end
80         
81         cursor:foreach(UCINAME, "daemon", function(config)
82                 if config.enabled ~= "1" then
83                         return
84                 end
85         
86                 local key = config[".name"]
87                 if not config.slave then
88                         nixio.syslog("crit", "Daemon "..key.." is missing a slave\n")
89                         os.exit(1)
90                 else
91                         nixio.syslog("info", "Initializing daemon " .. key)
92                 end
93                 
94                 state:revert(UCINAME, key)
95                 
96                 local daemon, code, err = prepare_daemon(config)
97                 if daemon then
98                         state:set(UCINAME, key, "status", "started")
99                         nixio.syslog("info", "Prepared daemon " .. key)
100                 else
101                         state:set(UCINAME, key, "status", "error")
102                         state:set(UCINAME, key, "error", err)
103                         nixio.syslog("err", "Failed to initialize daemon "..key..": "..
104                         err .. "\n")
105                 end
106         end)
107 end
108         
109 --- Run the superprocess if prepared before. 
110 -- This main function of LuCId will wait for events on given file descriptors.
111 function run()
112         local pollint = tonumber((cursor:get(UCINAME, "main", "pollinterval")))
113         local threadlimit = tonumber(cursor:get(UCINAME, "main", "threadlimit"))
114
115         while true do
116                 if not threadlimit or tcount < threadlimit then
117                         local stat, code = nixio.poll(pollt, pollint)
118                 
119                         if stat and stat > 0 then
120                                 for _, polle in ipairs(pollt) do
121                                         if polle.revents ~= 0 and polle.handler then
122                                                 polle.handler(polle)
123                                         end
124                                 end
125                         elseif stat == 0 then
126                                 ifaddrs = nixio.getifaddrs()
127                                 collectgarbage("collect")
128                         end
129                 end
130                 
131                 for _, cb in ipairs(tickt) do
132                         cb()
133                 end
134                 
135                 local pid, stat, code = nixio.wait(-1, "nohang")
136                 while pid and pid > 0 do
137                         tcount = tcount - 1
138                         if tpids[pid] and tpids[pid] ~= true then
139                                 tpids[pid](pid, stat, code)
140                         end
141                         pid, stat, code = nixio.wait(-1, "nohang")
142                 end
143         end
144 end
145
146 --- Add a file descriptor for the main loop and associate handler functions.
147 -- @param polle Table containing: {fd = FILE DESCRIPTOR, events = POLL EVENTS,
148 -- handler = EVENT HANDLER CALLBACK}
149 -- @see unregister_pollfd
150 -- @return boolean status
151 function register_pollfd(polle)
152         pollt[#pollt+1] = polle
153         return true 
154 end
155
156 --- Unregister a file desciptor and associate handler from the main loop.
157 -- @param polle Poll descriptor
158 -- @see register_pollfd
159 -- @return boolean status
160 function unregister_pollfd(polle)
161         for k, v in ipairs(pollt) do
162                 if v == polle then
163                         table.remove(pollt, k)
164                         return true
165                 end
166         end
167         return false
168 end
169
170 --- Close all registered file descriptors from main loop.
171 -- This is useful for forked child processes. 
172 function close_pollfds()
173         for k, v in ipairs(pollt) do
174                 if v.fd and v.fd.close then
175                         v.fd:close()
176                 end
177         end
178 end
179
180 --- Register a tick function that will be called at each cycle of the main loop.
181 -- @param cb Callback
182 -- @see unregister_tick
183 -- @return boolean status
184 function register_tick(cb)
185         tickt[#tickt+1] = cb
186         return true
187 end
188
189 --- Unregister a tick function from the main loop.
190 -- @param cb Callback
191 -- @see register_tick
192 -- @return boolean status
193 function unregister_tick(cb)
194         for k, v in ipairs(tickt) do
195                 if v == cb then
196                         table.remove(tickt, k)
197                         return true
198                 end
199         end
200         return false
201 end
202
203 --- Create a new child process from a Lua function and assign a destructor.
204 -- @param threadcb main function of the new process
205 -- @param waitcb destructor callback
206 -- @return process identifier or nil, error code, error message
207 function create_process(threadcb, waitcb)
208         local threadlimit = tonumber(cursor:get(UCINAME, "main", "threadlimit"))
209         if threadlimit and tcount >= threadlimit then
210                 nixio.syslog("warning", "Cannot create thread: process limit reached")
211                 return nil
212         end
213         local pid, code, err = nixio.fork()
214         if pid and pid ~= 0 then
215                 tpids[pid] = waitcb
216                 tcount = tcount + 1
217         elseif pid == 0 then
218                 local code = threadcb()
219                 os.exit(code)
220         else
221                 nixio.syslog("err", "Unable to fork(): " .. err)
222         end
223         return pid, code, err
224 end
225
226 --- Prepare a daemon from a given configuration table.
227 -- @param config Configuration data.
228 -- @return boolean status or nil, error code, error message
229 function prepare_daemon(config)
230         nixio.syslog("info", "Preparing daemon " .. config[".name"])
231         local modname = cursor:get(UCINAME, config.slave)
232         if not modname then
233                 return nil, -1, "invalid slave"
234         end
235
236         local stat, module = pcall(require, _NAME .. "." .. modname)
237         if not stat or not module.prepare_daemon then
238                 return nil, -2, "slave type not supported"
239         end
240         
241         config.slave = prepare_slave(config.slave)
242
243         return module.prepare_daemon(config, _M)
244 end
245
246 --- Prepare a slave.
247 -- @param name slave name
248 -- @return table containing slave module and configuration or nil, error message
249 function prepare_slave(name)
250         local slave = slaves[name]
251         if not slave then
252                 local config = cursor:get_all(UCINAME, name)
253                 
254                 local stat, module = pcall(require, config and config.entrypoint)
255                 if stat then
256                         slave = {module = module, config = config}
257                 end
258         end
259         
260         if slave then
261                 return slave
262         else
263                 return nil, module
264         end
265 end
266
267 --- Return a list of available network interfaces on the host.
268 -- @return table returned by nixio.getifaddrs()
269 function get_interfaces()
270         return ifaddrs
271 end
272
273 --- Revoke process privileges.
274 -- @param user new user name or uid
275 -- @param group new group name or gid
276 -- @return boolean status or nil, error code, error message
277 function revoke_privileges(user, group)
278         if nixio.getuid() == 0 then
279                 return nixio.setgid(group) and nixio.setuid(user)
280         end
281 end
282
283 --- Return a secure UCI cursor.
284 -- @return UCI cursor
285 function securestate()
286         local stat = nixio.fs.stat(SSTATE) or {}
287         local uid = nixio.getuid()
288         if stat.type ~= "dir" or (stat.modedec % 100) ~= 0 or stat.uid ~= uid then
289                 nixio.fs.remover(SSTATE)
290                 if not nixio.fs.mkdir(SSTATE, 700) then
291                         local errno = nixio.errno()
292                         nixio.syslog("err", "Integrity check on secure state failed!")
293                         return nil, errno, nixio.perror(errno)
294                 end
295         end
296         
297         return uci.cursor(nil, SSTATE)
298 end
299
300 --- Daemonize the process.
301 -- @return boolean status or nil, error code, error message
302 function daemonize()
303         if nixio.getppid() == 1 then
304                 return
305         end
306         
307         local pid, code, msg = nixio.fork()
308         if not pid then
309                 return nil, code, msg
310         elseif pid > 0 then
311                 os.exit(0)
312         end
313         
314         nixio.setsid()
315         nixio.chdir("/")
316         
317         local devnull = nixio.open("/dev/null", nixio.open_flags("rdwr"))
318         nixio.dup(devnull, nixio.stdin)
319         nixio.dup(devnull, nixio.stdout)
320         nixio.dup(devnull, nixio.stderr)
321         
322         return true
323 end