add ucitrigger: a uci plugin, command line tool and lua interface for automatically...
[openwrt.git] / package / uci / trigger / lib / trigger.lua
1 module("uci.trigger", package.seeall)
2 require("posix")
3 require("uci")
4
5 local path = "/lib/config/trigger"
6 local triggers = nil
7 local tmp_cursor = nil
8
9 function load_modules()
10         if triggers ~= nil then
11                 return
12         end
13         triggers = {
14                 list = {},
15                 uci = {},
16                 active = {}
17         }
18         local modules = posix.glob(path .. "/*.lua")
19         if modules == nil then
20                 return
21         end
22         local oldpath = package.path
23         package.path = path .. "/?.lua"
24         for i, v in ipairs(modules) do
25                 pcall(require(string.gsub(v, path .. "/(%w+)%.lua$", "%1")))
26         end
27         package.path = oldpath
28 end
29
30 function check_table(table, name)
31         if table[name] == nil then
32                 table[name] = {}
33         end
34         return table[name]
35 end
36
37 function get_table_val(val, vtype)
38         if type(val) == (vtype or "string") then
39                 return { val }
40         elseif type(val) == "table" then
41                 return val
42         end
43         return nil
44 end
45
46 function get_name_list(name)
47         return get_table_val(name or ".all")
48 end
49
50 function add_trigger_option(list, t)
51         local name = get_name_list(t.option)
52         for i, n in ipairs(name) do
53                 option = check_table(list, n)
54                 table.insert(option, t)
55         end
56 end
57
58 function add_trigger_section(list, t)
59         local name = get_name_list(t.section)
60         for i, n in ipairs(name) do
61                 section = check_table(list, n)
62                 add_trigger_option(section, t)
63         end
64 end
65
66 function check_insert_triggers(dest, list, tuple)
67         if list == nil then
68                 return
69         end
70         for i, t in ipairs(list) do
71                 local add = true
72                 if type(t.check) == "function" then
73                         add = t.check(tuple)
74                 end
75                 if add then
76                         dest[t.id] = t
77                 end
78         end
79 end
80
81 function find_section_triggers(tlist, pos, tuple)
82         if pos == nil then
83                 return
84         end
85         check_insert_triggers(tlist, pos[".all"], tuple)
86         if tuple.option then
87                 check_insert_triggers(tlist, pos[tuple.option], tuple)
88         end
89 end
90
91 function check_recursion(name, seen)
92         if seen == nil then
93                 seen = {}
94         end
95         if seen[name] then
96                 return nil
97         end
98         seen[name] = true
99         return seen
100 end
101
102
103 function find_recursive_depends(list, name, seen)
104         seen = check_recursion(name, seen)
105         if not seen then
106                 return
107         end
108         local bt = get_table_val(triggers.list[name].belongs_to) or {}
109         for i, n in ipairs(bt) do
110                 table.insert(list, n)
111                 find_recursive_depends(list, n, seen)
112         end
113 end
114
115 function check_trigger_depth(list, name)
116         if name == nil then
117                 return
118         end
119
120         local n = list[name]
121         if n == nil then
122                 return
123         end
124
125         list[name] = nil
126         return check_trigger_depth(list, n)
127 end
128
129 function find_triggers(tuple)
130         local pos = triggers.uci[tuple.package]
131         if pos == nil then
132                 return {}
133         end
134
135         local tlist = {}
136         find_section_triggers(tlist, pos[".all"], tuple)
137         find_section_triggers(tlist, pos[tuple.section[".type"]], tuple)
138
139         for n, t in pairs(tlist) do
140                 local dep = {}
141                 find_recursive_depends(dep, t.id)
142                 for i, depname in ipairs(dep) do
143                         check_trigger_depth(tlist, depname)
144                 end
145         end
146
147         local nlist = {}
148         for n, t in pairs(tlist) do
149                 if t then
150                         table.insert(nlist, t)
151                 end
152         end
153
154         return nlist
155 end
156
157 function reset_state()
158         assert(io.open("/var/run/uci_trigger", "w")):close()
159         if tctx then
160                 tctx:unload("uci_trigger")
161         end
162 end
163
164 function load_state()
165         -- make sure the config file exists before we attempt to load it
166         -- uci doesn't like loading nonexistent config files
167         local f = assert(io.open("/var/run/uci_trigger", "a")):close()
168
169         load_modules()
170         triggers.active = {}
171         if tctx then
172                 tctx:unload("uci_trigger")
173         else
174                 tctx = uci.cursor()
175         end
176         assert(tctx:load("/var/run/uci_trigger"))
177         tctx:foreach("uci_trigger", "trigger",
178                 function(section)
179                         trigger = triggers.list[section[".name"]]
180                         if trigger == nil then
181                                 return
182                         end
183
184                         active = {}
185                         triggers.active[trigger.id] = active
186
187                         local s = get_table_val(section["sections"]) or {}
188                         for i, v in ipairs(s) do
189                                 active[v] = true
190                         end
191                 end
192         )
193 end
194
195 function get_names(list)
196         local slist = {}
197         for name, val in pairs(list) do
198                 if val then
199                         table.insert(slist, name)
200                 end
201         end
202         return slist
203 end
204
205 function check_cancel(name, seen)
206         local t = triggers.list[name]
207         local dep = get_table_val(t.belongs_to)
208         seen = check_recursion(name, seen)
209
210         if not t or not dep or not seen then
211                 return false
212         end
213
214         for i, v in ipairs(dep) do
215                 -- only cancel triggers for all sections
216                 -- if both the current and the parent trigger
217                 -- are per-section
218                 local section_only = false
219                 if t.section_only then
220                         local tdep = triggers.list[v]
221                         if tdep then
222                                 section_only = tdep.section_only
223                         end
224                 end
225
226                 if check_cancel(v, seen) then
227                         return true
228                 end
229                 if triggers.active[v] then
230                         if section_only then
231                                 for n, active in pairs(triggers.active[v]) do
232                                         triggers.active[name][n] = false
233                                 end
234                         else
235                                 return true
236                         end
237                 end
238         end
239         return false
240 end
241
242 -- trigger api functions
243
244 function add(ts)
245         for i,t in ipairs(ts) do
246                 triggers.list[t.id] = t
247                 match = {}
248                 if t.package then
249                         local package = check_table(triggers.uci, t.package)
250                         add_trigger_section(package, t)
251                         triggers.list[t.id] = t
252                 end
253         end
254 end
255
256 function set(data, cursor)
257         assert(data ~= nil)
258         if cursor == nil then
259                 cursor = tmp_cursor or uci.cursor()
260                 tmp_cursor = uci.cursor
261         end
262
263         local tuple = {
264                 package = data[1],
265                 section = data[2],
266                 option = data[3],
267                 value = data[4]
268         }
269         assert(cursor:load(tuple.package))
270
271         load_state()
272         local section = cursor:get_all(tuple.package, tuple.section)
273         if (section == nil) then
274                 if option ~= nil then
275                         return
276                 end
277                 section = {
278                         [".type"] = value
279                 }
280                 if tuple.section == nil then
281                         tuple.section = ""
282                         section[".anonymous"] = true
283                 end
284                 section[".name"] = tuple.section
285         end
286         tuple.section = section
287
288         local ts = find_triggers(tuple)
289         for i, t in ipairs(ts) do
290                 local active = triggers.active[t.id]
291                 if not active then
292                         active = {}
293                         triggers.active[t.id] = active
294                         tctx:set("uci_trigger", t.id, "trigger")
295                 end
296                 if section[".name"] then
297                         active[section[".name"]] = true
298                 end
299                 local slist = get_names(triggers.active[t.id])
300                 if #slist > 0 then
301                         tctx:set("uci_trigger", t.id, "sections", slist)
302                 end
303         end
304         tctx:save("uci_trigger")
305 end
306
307 function get_description(trigger, sections)
308         if not trigger.title then
309                 return trigger.id
310         end
311         local desc = trigger.title
312         if trigger.section_only and sections and #sections > 0 then
313                 desc = desc .. " (" .. table.concat(sections, ", ") .. ")"
314         end
315         return desc
316 end
317
318 function get_active()
319         local slist = {}
320
321         if triggers == nil then
322                 load_state()
323         end
324         for name, val in pairs(triggers.active) do
325                 if val and not check_cancel(name) then
326                         local sections = {}
327                         for name, active in pairs(triggers.active[name]) do
328                                 if active then
329                                         table.insert(sections, name)
330                                 end
331                         end
332                         table.insert(slist, { triggers.list[name], sections })
333                 end
334         end
335         return slist
336 end
337
338 function run(ts)
339         if ts == nil then
340                 ts = get_active()
341         end
342         for i, t in ipairs(ts) do
343                 local trigger = t[1]
344                 local sections = t[2]
345                 local actions = get_table_val(trigger.action, "function") or {}
346                 for ai, a in ipairs(actions) do
347                         if not trigger.section_only then
348                                 sections = { "" }
349                         end
350                         for si, s in ipairs(sections) do
351                                 if a(s) then
352                                         tctx:delete("uci_trigger", trigger.id)
353                                         tctx:save("uci_trigger")
354                                 end
355                         end
356                 end
357         end
358 end
359
360 -- helper functions
361
362 function system_command(arg)
363         local cmd = arg
364         return function(arg)
365                 return os.execute(cmd:format(arg)) == 0
366         end
367 end
368
369 function service_restart(arg)
370         return system_command("/etc/init.d/" .. arg .. " restart")
371 end