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