summaryrefslogtreecommitdiffhomepage
path: root/libs/lucid/luasrc/lucid.lua
blob: d743269fe3fbe2c4c0a2e3f7c3a75dcbddc9a15e (plain)
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
--[[
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()
	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

--- 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()
			collectgarbage("collect")
		end
		
		for _, cb in ipairs(tickt) do
			cb()
		end
		
		local pid, stat, code = nixio.wait(-1, "nohang")
		while pid and pid > 0 do
			tcount = tcount - 1
			if tpids[pid] and tpids[pid] ~= true then
				tpids[pid](pid, stat, code)
			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
	end
	local pid, code, err = nixio.fork()
	if pid and pid ~= 0 then
		tpids[pid] = waitcb
		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