Reduce differences with root_skels in contrib.
[dragonfly.git] / contrib / bsdinstaller-1.1.6 / src / lib / lua / app / app.lua
blobcda7581fe299a828e94a6cde6d7c572fef014f0d
1 -- app.lua
2 -- $Id: app.lua,v 1.52 2005/04/03 20:59:42 cpressey Exp $
3 -- Lua-based Application Environment static object.
5 -- BEGIN app.lua --
7 module("app")
9 local POSIX = require("posix")
10 local FileName = require("filename")
11 local Pty = require("pty")
13 --[[-----]]--
14 --[[ App ]]--
15 --[[-----]]--
17 -- Application Environment - roughly equivalent to
18 -- InstallerContext (or i_fn_args in the C version,) but:
20 -- * this version is written purely in Lua, and
21 -- * this version is not specific to the Installer - it could just as well
22 -- be used for any application that needs:
24 -- o user interface facilities (highly abstracted)
25 -- o configuration, possibly loaded from config files
26 -- - locations of directories (root dir, temp dir, etc)
27 -- - names of system commands
28 -- - etc
29 -- o application-wide options
30 -- o application-wide state
31 -- o logging
32 -- o temporary files
34 -- For simplicity, we consider this to be a singleton or
35 -- "static object" (with a single global "instance" called App.)
37 App = {}
40 -- Initialize global stuff.
43 App.init = function()
44 App.defaults = {
45 name = "Unnamed Application",
46 logfile = "unnamed.log",
47 dir = {
48 root = "/",
49 tmp = "/tmp/"
51 transport = "tcp",
52 rendezvous = "9999"
55 App.last_log_time = -1
56 App.conf_path = ""
57 App.current_script = arg[0]
59 App.config = {}
60 App.option = {}
61 App.state = {}
63 App.add_pkg_path("./lib")
64 App.add_pkg_path(FileName.dirname(App.current_script) .. "lib")
65 App.add_conf_path("./conf")
66 App.add_conf_path(FileName.dirname(App.current_script) .. "conf")
68 arg = App.process_cmdline(arg)
69 end
72 -- Startup and shutdown.
75 App.start = function(opt)
76 local k, v
79 -- Private function to create a dummy user interface adapter
80 -- if the App was started without one.
82 local new_dummy_ui = function()
83 local method = {}
85 method.start = function(method)
86 App.log("Dummy user interface started")
87 return true
88 end
90 method.stop = function(method)
91 App.log("Dummy user interface stopped")
92 return true
93 end
95 method.present = function(method, tab)
96 App.dump_table(tab)
97 return {
98 action_id = tab.actions[1].id,
99 datasets = tab.datasets
103 method.inform = function(method, msg)
104 App.log("INFORM: %s", msg)
105 return { action_id = "ok", datasets = {} }
108 method.confirm = function(method, msg)
109 App.log("CONFIRM: %s", msg)
110 return true
113 method.select = function(method, msg, map)
114 local k, v
115 App.log("SELECT: %s", msg)
116 for k, v in map do
117 return v
121 method.select_file = function(method, tab)
122 App.log("SELECT FILE: %s", tab.title or "Select File")
123 return "cancel"
127 -- Constructor within a constructor, here...
129 method.new_progress_bar = function(method, tab)
130 local method = {}
132 method.start = function(method)
133 App.log("START PROGRESS BAR")
134 return true
137 method.set_amount = function(method, new_amount)
138 App.log("SET PROGRESS AMOUNT: %d", new_amount)
139 return true
142 method.set_short_desc = function(method, new_short_desc)
143 App.log("SET PROGRESS DESC: %d", new_short_desc)
144 return true
147 method.update = function(method)
148 App.log("PROGRESS UPDATE: %d", new_amount)
149 return true
152 method.stop = function(method)
153 App.log("STOP PROGRESS BAR")
154 return true
157 return method
160 return method
164 -- Begin setting up the App.
167 -- Set up defaults.
168 if not opt then
169 opt = {}
172 App.merge_tables(opt, App.defaults, function(key, dest_val, src_val)
173 if not dest_val then
174 return src_val
175 else
176 return dest_val
178 end)
180 -- Set name of application.
181 App.name = opt.name
182 App.log_filename = opt.logfile
184 -- Set up directories, and make sure each ends with a slash.
185 App.dir = opt.dir
186 for k, v in App.dir do
187 if string.sub(v, -1) ~= "/" then
188 App.dir[k] = v .. "/"
192 -- Determine the operating system.
193 App.os = {}
194 App.os.name = App.determine_os_name()
195 -- App.os.version = App.determine_os_version()
197 -- Open our logfile.
198 App.open_log(App.dir.tmp .. App.log_filename)
199 App.log(App.name .. " started")
201 -- Load command names, if available.
202 App.cmd_names = App.load_conf("cmdnames")
204 -- Set up the ${}-expansion function.
205 App.expand = function(str, ...)
206 local ltables = arg or {}
207 local gtables = {App.cmd_names, App.dir}
209 local result = string.gsub(str, "%$%{([%w_]+)%}", function(key)
210 local i, tab, value
212 if table.getn(ltables) > 0 then
213 for i, tab in ipairs(ltables) do
214 value = tab[key]
215 if value then
216 return value
221 if table.getn(gtables) > 0 then
222 for i, tab in ipairs(gtables) do
223 value = tab[key]
224 if value then
225 return value
230 App.log_warn("Could not expand `${%s}'", key)
231 return "${" .. key .. "}"
232 end)
234 return result
237 -- Set up temporary files.
238 App.tmpfile = {}
240 -- Set up application-specific containers:
241 -- config: application configuration
242 -- option: application-wide options
243 -- state: application-wide state
244 App.config = opt.config or App.config
245 App.option = opt.option or App.option
246 App.state = opt.state or App.state
248 -- Seed the random-number generator.
249 math.randomseed(os.time())
251 -- Set up the App's UI adapter.
252 App.ui = opt.ui or new_dummy_ui()
253 if not App.ui:start() then
254 App.log_fatal("Could not start user interface")
258 App.stop = function()
259 App.clean_tmpfiles()
260 App.ui:stop()
261 App.log("Shutting down")
262 App.close_log()
265 App.process_cmdline = function(arg)
266 local argn = 1
267 local remaining_arg = {}
269 while arg[argn] do
270 if arg[argn] == "-C" then
271 argn = argn + 1
272 App.add_conf_path(arg[argn])
273 elseif arg[argn] == "-L" then
274 argn = argn + 1
275 App.add_pkg_path(arg[argn])
276 elseif arg[argn] == "-R" then
277 argn = argn + 1
278 local script_name = App.find_script(arg[argn]) or arg[argn]
279 local ok, result = App.run(script_name)
280 if not ok then
281 io.stderr:write("warning: could not run `" ..
282 tostring(script_name) .. "':\n")
283 io.stderr:write(result .. "\n")
285 elseif string.find(arg[argn], "=") then
286 App.set_property(arg[argn])
287 else
288 table.insert(remaining_arg, arg[argn])
291 argn = argn + 1
294 return remaining_arg
298 -- Given a string in the form "foo.bar=baz", set the member "bar" of the
299 -- subtable "foo" of the App object to "baz".
301 App.set_property = function(expr)
302 local found, len, k, v, c, r, i, t
304 t = App.defaults
305 r = {}
306 found, len, k, v = string.find(expr, "^(.*)=(.*)$")
307 for c in string.gfind(k, "[^%.]+") do
308 table.insert(r, c)
310 for i, c in r do
311 if i == table.getn(r) then
312 t[c] = v
313 else
314 if not t[c] then
315 t[c] = {}
317 if type(t[c]) == "table" then
318 t = t[c]
319 else
320 App.log_warn("%s: not a table", tostring(c))
327 -- Add a directory to package.path (used by compat-5.1.)
329 App.add_pkg_path = function(dir)
330 if package and package.path then
331 if package.path ~= "" then
332 package.path = package.path .. ";"
334 package.path = package.path .. tostring(dir) .. "/?.lua"
339 -- Add a directory to App.conf_path (used by App.load_conf().)
341 App.add_conf_path = function(dir)
342 if App.conf_path ~= "" then
343 App.conf_path = App.conf_path .. ";"
345 App.conf_path = App.conf_path .. tostring(dir) .. "/?.lua"
349 -- Run a Lua script.
350 -- Note that the script name must be either relative to the
351 -- current working directory, or fully-qualified.
352 -- If relative to the current script, use App.find_script first.
353 -- This function returns two values:
354 -- the first is the success code, either true or false
355 -- if true, the second is the result of the script
356 -- if false, the second is an error message string.
358 App.run = function(script_name, ...)
359 local save_script = App.current_script
360 local save_args = ARG
361 local ok, result, fun, errmsg
363 if App.option.fatal_errors then
364 assert(script_name and type(script_name) == "string",
365 "bad filename " .. tostring(script_name))
367 if not script_name or type(script_name) ~= "string" then
368 return false, "bad filename " .. tostring(script_name)
371 App.add_pkg_path(FileName.dirname(script_name) .. "lib")
372 App.add_conf_path(FileName.dirname(script_name) .. "conf")
374 fun, errmsg = loadfile(script_name)
376 if App.option.fatal_errors then
377 assert(fun, errmsg)
379 if not fun then
380 return false, errmsg
383 App.current_script = script_name
384 ARG = arg
385 if App.option.fatal_errors then
386 ok = true
387 result = fun()
388 else
389 ok, result = pcall(fun)
391 ARG = save_args
392 App.current_script = save_script
394 return ok, result
398 -- Find a Lua script.
400 App.find_script = function(script_name)
401 script_name = FileName.dirname(App.current_script) .. script_name
403 if FileName.is_dir(script_name) then
404 if string.sub(script_name, -1, -1) ~= "/" then
405 script_name = script_name .. "/"
407 return script_name .. "main.lua"
408 elseif FileName.is_file(script_name) then
410 -- Just execute that script.
412 return script_name
413 else
415 -- Couldn't find it relative to the current script.
417 io.stderr:write("WARNING: could not find `" .. script_name .. "'\n")
418 return nil
423 -- Dump the contents of the given table to stdout,
424 -- primarily intended for debugging.
426 App.dump_table = function(tab, indent)
427 local k, v
429 if not indent then
430 indent = ""
433 for k, v in tab do
434 if type(v) == "table" then
435 print(indent .. tostring(k) .. "=")
436 App.dump_table(v, indent .. "\t")
437 else
438 print(indent .. tostring(k) .. "=" .. tostring(v))
444 -- Merge two tables by looking at each item from the second (src)
445 -- table and putting a value into the first (dest) table based on
446 -- the result of a provided callback function which receives the
447 -- key and bother values, and returns the resulting value.
449 -- An 'overriding' merge can be accomplished with:
450 -- function(key, dest_val, src_val)
451 -- return src_val
452 -- end
454 -- A 'non-overriding' merge can be accomplished with:
455 -- function(key, dest_val, src_val)
456 -- if dest_val == nil then
457 -- return src_val
458 -- else
459 -- return dest_val
460 -- end
461 -- end
463 App.merge_tables = function(dest, src, fun)
464 local k, v
466 for k, v in src do
467 if type(v) == "table" then
468 if not dest[k] then
469 dest[k] = {}
471 if type(dest[k]) == "table" then
472 App.merge_tables(dest[k], v, fun)
474 else
475 dest[k] = fun(k, dest[k], v)
481 -- Run a script. Expects the full filename (will not search.)
482 -- Displays a nice dialog box if the script contained errors.
484 App.run_script = function(script_name, ...)
485 local ok, result = App.run(script_name, unpack(arg))
486 if ok then
487 return result
489 App.log_warn("Error occurred while loading script `" ..
490 tostring(script_name) .. "': " .. tostring(result))
491 if App.ui then
492 App.ui:present{
493 id = "script_error",
494 name = "Error Loading Script",
495 short_desc =
496 "An internal Lua error occurred while " ..
497 "trying to run the script " ..
498 tostring(script_name) .. ":\n\n" ..
499 tostring(result),
500 role = "alert",
501 actions = {
503 id = "ok",
504 name = "OK"
509 return nil
513 -- Run a sub-application (a script relative to the current script.)
515 App.descend = function(script_name, ...)
516 return App.run_script(App.find_script(script_name), unpack(arg))
520 -- Wait for a condition to come true.
521 -- Display a (cancellable) progress bar while we wait.
522 -- Returns two values: whether the condition eventually
523 -- did come true, and roughly how long it took (if it
524 -- timed out, this value will be greater than the timeout.)
526 App.wait_for = function(tab)
527 local predicate = tab.predicate
528 local timeout = tab.timeout or 30
529 local frequency = tab.frequency or 2
530 local title = tab.title or "Please wait..."
531 local short_desc = tab.short_desc or title
532 local pr
533 local time_elapsed = 0
534 local cancelled = false
536 assert(type(predicate) == "function")
538 if predicate() then
539 return true
542 pr = App.ui:new_progress_bar{
543 title = title,
544 short_desc = short_desc
546 pr:start()
548 while time_elapsed < timeout and not cancelled and not result do
549 POSIX.nanosleep(frequency)
550 time_elapsed = time_elapsed + frequency
551 if predicate() then
552 return true, time_elapsed
554 pr:set_amount((time_elapsed * 100) / timeout)
555 cancelled = not pr:update()
558 pr:stop()
560 return false, time_elapsed
564 -- Configuration file loading.
567 App.locate_conf = function(name)
568 local comp
570 for comp in string.gfind(App.conf_path, "[^;]+") do
571 comp = string.gsub(comp, "?", name)
572 if FileName.is_file(comp) then
573 return comp
577 return nil
580 App.load_conf = function(name)
581 local filename = App.locate_conf(name)
583 if filename ~= nil then
584 App.log("Loading configuration file '%s'...", filename)
585 return App.run_script(filename)
586 else
587 App.log_warn("Could not locate configuration file '%s'!", name)
588 return nil
593 -- Logging.
596 App.open_log = function(filename, mode)
597 if App.log_file then
598 return
600 if not mode then
601 mode = "w"
603 local fh, err = io.open(filename, mode)
604 App.log_file = nil
605 if fh then
606 App.log_file = fh
607 else
608 error(err)
612 App.close_log = function()
613 if App.log_file then
614 App.log_file:close()
615 App.log_file = nil
619 App.log = function(str, ...)
620 local stamp = math.floor(os.time())
621 local line = ""
623 local write_log = function(s)
624 s = s .. "\n"
625 io.stderr:write(s)
626 if App.log_file then
627 App.log_file:write(s)
628 App.log_file:flush()
632 if stamp > App.last_log_time then
633 App.last_log_time = stamp
634 write_log("[" .. os.date() .. "]")
637 write_log(string.format(str, unpack(arg)))
640 App.log_warn = function(str, ...)
641 App.log("WARNING: " .. str, unpack(arg))
644 App.log_fatal = function(str, ...)
645 App.log(str, unpack(arg))
646 error(str)
649 App.view_log = function()
650 local contents = ""
651 local fh
653 App.close_log()
655 fh = io.open(App.dir.tmp .. App.log_filename, "r")
656 for line in fh:lines() do
657 contents = contents .. line .. "\n"
659 fh:close()
661 App.ui:present({
662 id = "app_log",
663 name = App.name .. ": Log",
664 short_desc = contents,
665 role = "informative",
666 minimum_width = "72",
667 monospaced = "true",
668 actions = {
669 { id = "ok", name = "OK" }
673 App.open_log(App.dir.tmp .. App.log_filename, "a")
677 -- Temporary file handling.
680 App.clean_tmpfiles = function()
681 local filename, unused
683 for filename, unused in App.tmpfile do
684 App.log("Deleting tmpfile: " .. filename)
685 os.remove(App.dir.tmp .. filename)
689 -- Registers that the given file (which resides in App.dir.tmp)
690 -- is a temporary file, and may be deleted when upon exit.
691 App.register_tmpfile = function(filename)
692 App.tmpfile[filename] = 1
695 -- Creates and opens a new temporary file (in App.dir.tmp).
696 -- If the filename is omitted, one is chosen using the mkstemp
697 -- system call. If the mode is omitted, updating ("w+") is
698 -- assumed. The file object and the file name are returned.
699 App.open_tmpfile = function(filename, mode)
700 local fh, err
702 if not filename then
703 fh, filename = POSIX.mkstemp(App.dir.tmp .. "Lua.XXXXXXXX")
704 filename = FileName.basename(filename)
705 else
706 fh, err = io.open(App.dir.tmp .. filename, mode or "w+")
707 if err then
708 return nil, err
711 App.register_tmpfile(filename)
712 return fh, filename
716 -- Operating system determination.
717 -- NOTE: this is pretty weak - this is before we have
718 -- loaded the command locations, and sysctl could be anywhere on path.
719 -- Besides, this should be overridable somehow on principle.
720 -- Perhaps even hard-coded.
723 App.determine_os_name = function()
724 local pty = Pty.open("sysctl -n kern.ostype")
725 local osname = pty:readline()
726 pty:close()
727 return osname
731 -- More debugging.
732 -- Install logging wrappers around every method in a class/object.
734 App.log_methods = function(obj_method_table)
735 local k, v
736 for k, v in pairs(obj_method_table) do
737 local method_name, orig_fun = k, method[k]
738 method[k] = function(...)
739 App.log("ENTERING: %s", method_name)
740 orig_fun(unpack(arg))
741 App.log("EXITED: %s", method_name)
746 return App
748 -- END of lib/app.lua --