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