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