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