LuCId: Watch threads in debug mode
[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         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                         collectgarbage("collect")
140                 end
141                 
142                 for _, cb in ipairs(tickt) do
143                         cb()
144                 end
145                 
146                 local pid, stat, code = nixio.wait(-1, "nohang")
147                 while pid and pid > 0 do
148                         nixio.syslog("info", "Buried thread: " .. pid)
149                         if tpids[pid] then
150                                 tcount = tcount - 1
151                                 if tpids[pid] ~= true then
152                                         tpids[pid](pid, stat, code)
153                                 end
154                         end
155                         pid, stat, code = nixio.wait(-1, "nohang")
156                 end
157         end
158 end
159
160 --- Add a file descriptor for the main loop and associate handler functions.
161 -- @param polle Table containing: {fd = FILE DESCRIPTOR, events = POLL EVENTS,
162 -- handler = EVENT HANDLER CALLBACK}
163 -- @see unregister_pollfd
164 -- @return boolean status
165 function register_pollfd(polle)
166         pollt[#pollt+1] = polle
167         return true 
168 end
169
170 --- Unregister a file desciptor and associate handler from the main loop.
171 -- @param polle Poll descriptor
172 -- @see register_pollfd
173 -- @return boolean status
174 function unregister_pollfd(polle)
175         for k, v in ipairs(pollt) do
176                 if v == polle then
177                         table.remove(pollt, k)
178                         return true
179                 end
180         end
181         return false
182 end
183
184 --- Close all registered file descriptors from main loop.
185 -- This is useful for forked child processes. 
186 function close_pollfds()
187         for k, v in ipairs(pollt) do
188                 if v.fd and v.fd.close then
189                         v.fd:close()
190                 end
191         end
192 end
193
194 --- Register a tick function that will be called at each cycle of the main loop.
195 -- @param cb Callback
196 -- @see unregister_tick
197 -- @return boolean status
198 function register_tick(cb)
199         tickt[#tickt+1] = cb
200         return true
201 end
202
203 --- Unregister a tick function from the main loop.
204 -- @param cb Callback
205 -- @see register_tick
206 -- @return boolean status
207 function unregister_tick(cb)
208         for k, v in ipairs(tickt) do
209                 if v == cb then
210                         table.remove(tickt, k)
211                         return true
212                 end
213         end
214         return false
215 end
216
217 --- Tests whether a given number of processes can be created.
218 -- @oaram num Processes to be created
219 -- @return boolean status
220 function try_process(num)
221         local threadlimit = tonumber((cursor:get(UCINAME, "main", "threadlimit")))
222         return not threadlimit or (threadlimit - tcount) >= (num or 1)
223 end
224
225 --- Create a new child process from a Lua function and assign a destructor.
226 -- @param threadcb main function of the new process
227 -- @param waitcb destructor callback
228 -- @return process identifier or nil, error code, error message
229 function create_process(threadcb, waitcb)
230         local threadlimit = tonumber(cursor:get(UCINAME, "main", "threadlimit"))
231         if threadlimit and tcount >= threadlimit then
232                 nixio.syslog("warning", "Cannot create thread: process limit reached")
233                 return nil
234         end
235         local pid, code, err = nixio.fork()
236         if pid and pid ~= 0 then
237                 nixio.syslog("info", "Created thread: " .. pid)
238                 tpids[pid] = waitcb or true
239                 tcount = tcount + 1
240         elseif pid == 0 then
241                 local code = threadcb()
242                 os.exit(code)
243         else
244                 nixio.syslog("err", "Unable to fork(): " .. err)
245         end
246         return pid, code, err
247 end
248
249 --- Prepare a daemon from a given configuration table.
250 -- @param config Configuration data.
251 -- @return boolean status or nil, error code, error message
252 function prepare_daemon(config)
253         nixio.syslog("info", "Preparing daemon " .. config[".name"])
254         local modname = cursor:get(UCINAME, config.slave)
255         if not modname then
256                 return nil, -1, "invalid slave"
257         end
258
259         local stat, module = pcall(require, _NAME .. "." .. modname)
260         if not stat or not module.prepare_daemon then
261                 return nil, -2, "slave type not supported"
262         end
263         
264         config.slave = prepare_slave(config.slave)
265
266         return module.prepare_daemon(config, _M)
267 end
268
269 --- Prepare a slave.
270 -- @param name slave name
271 -- @return table containing slave module and configuration or nil, error message
272 function prepare_slave(name)
273         local slave = slaves[name]
274         if not slave then
275                 local config = cursor:get_all(UCINAME, name)
276                 
277                 local stat, module = pcall(require, config and config.entrypoint)
278                 if stat then
279                         slave = {module = module, config = config}
280                 end
281         end
282         
283         if slave then
284                 return slave
285         else
286                 return nil, module
287         end
288 end
289
290 --- Return a list of available network interfaces on the host.
291 -- @return table returned by nixio.getifaddrs()
292 function get_interfaces()
293         return ifaddrs
294 end
295
296 --- Revoke process privileges.
297 -- @param user new user name or uid
298 -- @param group new group name or gid
299 -- @return boolean status or nil, error code, error message
300 function revoke_privileges(user, group)
301         if nixio.getuid() == 0 then
302                 return nixio.setgid(group) and nixio.setuid(user)
303         end
304 end
305
306 --- Return a secure UCI cursor.
307 -- @return UCI cursor
308 function securestate()
309         local stat = nixio.fs.stat(SSTATE) or {}
310         local uid = nixio.getuid()
311         if stat.type ~= "dir" or (stat.modedec % 100) ~= 0 or stat.uid ~= uid then
312                 nixio.fs.remover(SSTATE)
313                 if not nixio.fs.mkdir(SSTATE, 700) then
314                         local errno = nixio.errno()
315                         nixio.syslog("err", "Integrity check on secure state failed!")
316                         return nil, errno, nixio.perror(errno)
317                 end
318         end
319         
320         return uci.cursor(nil, SSTATE)
321 end
322
323 --- Daemonize the process.
324 -- @return boolean status or nil, error code, error message
325 function daemonize()
326         if nixio.getppid() == 1 then
327                 return
328         end
329         
330         local pid, code, msg = nixio.fork()
331         if not pid then
332                 return nil, code, msg
333         elseif pid > 0 then
334                 os.exit(0)
335         end
336         
337         nixio.setsid()
338         nixio.chdir("/")
339         
340         local devnull = nixio.open("/dev/null", nixio.open_flags("rdwr"))
341         nixio.dup(devnull, nixio.stdin)
342         nixio.dup(devnull, nixio.stdout)
343         nixio.dup(devnull, nixio.stderr)
344         
345         return true
346 end