ucitrigger: add options to force enable/disable specific triggers
[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 save_trigger(name)
257         if triggers.active[name] then
258                 local slist = get_names(triggers.active[name])
259                 if #slist > 0 then
260                         tctx:set("uci_trigger", name, "sections", slist)
261                 end
262         else
263                 tctx:delete("uci_trigger", name)
264         end
265 end
266
267 function set(data, cursor)
268         assert(data ~= nil)
269         if cursor == nil then
270                 cursor = tmp_cursor or uci.cursor()
271                 tmp_cursor = uci.cursor
272         end
273
274         local tuple = {
275                 package = data[1],
276                 section = data[2],
277                 option = data[3],
278                 value = data[4]
279         }
280         assert(cursor:load(tuple.package))
281
282         load_state()
283         local section = cursor:get_all(tuple.package, tuple.section)
284         if (section == nil) then
285                 if option ~= nil then
286                         return
287                 end
288                 section = {
289                         [".type"] = value
290                 }
291                 if tuple.section == nil then
292                         tuple.section = ""
293                         section[".anonymous"] = true
294                 end
295                 section[".name"] = tuple.section
296         end
297         tuple.section = section
298
299         local ts = find_triggers(tuple)
300         for i, t in ipairs(ts) do
301                 local active = triggers.active[t.id]
302                 if not active then
303                         active = {}
304                         triggers.active[t.id] = active
305                         tctx:set("uci_trigger", t.id, "trigger")
306                 end
307                 if section[".name"] then
308                         active[section[".name"]] = true
309                 end
310                 save_trigger(t.id)
311         end
312         tctx:save("uci_trigger")
313 end
314
315 function get_description(trigger, sections)
316         if not trigger.title then
317                 return trigger.id
318         end
319         local desc = trigger.title
320         if trigger.section_only and sections and #sections > 0 then
321                 desc = desc .. " (" .. table.concat(sections, ", ") .. ")"
322         end
323         return desc
324 end
325
326 function get_active()
327         local slist = {}
328
329         if triggers == nil then
330                 load_state()
331         end
332         for name, val in pairs(triggers.active) do
333                 if val and not check_cancel(name) then
334                         local sections = {}
335                         for name, active in pairs(triggers.active[name]) do
336                                 if active then
337                                         table.insert(sections, name)
338                                 end
339                         end
340                         table.insert(slist, { triggers.list[name], sections })
341                 end
342         end
343         return slist
344 end
345
346 function set_active(trigger, sections)
347         if triggers == nil then
348                 load_state()
349         end
350         if not triggers.list[trigger] then
351                 return
352         end
353         if triggers.active[trigger] == nil then
354                 tctx:set("uci_trigger", trigger, "trigger")
355                 triggers.active[trigger] = {}
356         end
357         local active = triggers.active[trigger]
358         if triggers.list[trigger].section_only or sections ~= nil then
359                 for i, t in ipairs(sections) do
360                         triggers.active[trigger][t] = true
361                 end
362         end
363         save_trigger(trigger)
364         tctx:save("uci_trigger")
365 end
366
367 function clear_active(trigger, sections)
368         if triggers == nil then
369                 load_state()
370         end
371         if triggers.list[trigger] == nil or triggers.active[trigger] == nil then
372                 return
373         end
374         local active = triggers.active[trigger]
375         if not triggers.list[trigger].section_only or sections == nil then
376                 triggers.active[trigger] = nil
377         else
378                 for i, t in ipairs(sections) do
379                         triggers.active[trigger][t] = false
380                 end
381         end
382         save_trigger(trigger)
383         tctx:save("uci_trigger")
384 end
385
386 function run(ts)
387         if ts == nil then
388                 ts = get_active()
389         end
390         for i, t in ipairs(ts) do
391                 local trigger = t[1]
392                 local sections = t[2]
393                 local actions = get_table_val(trigger.action, "function") or {}
394                 for ai, a in ipairs(actions) do
395                         if not trigger.section_only then
396                                 sections = { "" }
397                         end
398                         for si, s in ipairs(sections) do
399                                 if a(s) then
400                                         tctx:delete("uci_trigger", trigger.id)
401                                         tctx:save("uci_trigger")
402                                 end
403                         end
404                 end
405         end
406 end
407
408 -- helper functions
409
410 function system_command(arg)
411         local cmd = arg
412         return function(arg)
413                 return os.execute(cmd:format(arg)) == 0
414         end
415 end
416
417 function service_restart(arg)
418         return system_command("/etc/init.d/" .. arg .. " restart")
419 end