2 -- $Id: app.lua,v 1.52 2005/04/03 20:59:42 cpressey Exp $
3 -- Lua-based Application Environment static object.
9 local POSIX = require("posix")
10 local FileName = require("filename")
11 local Pty = require("pty")
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
29 -- o application-wide options
30 -- o application-wide state
34 -- For simplicity, we consider this to be a singleton or
35 -- "static object" (with a single global "instance" called App.)
40 -- Initialize global stuff.
45 name = "Unnamed Application",
46 logfile = "unnamed.log",
55 App.last_log_time = -1
57 App.current_script = arg[0]
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)
72 -- Startup and shutdown.
75 App.start = function(opt)
79 -- Private function to create a dummy user interface adapter
80 -- if the App was started without one.
82 local new_dummy_ui = function()
85 method.start = function(method)
86 App.log("Dummy user interface started")
90 method.stop = function(method)
91 App.log("Dummy user interface stopped")
95 method.present = function(method, tab)
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)
113 method.select = function(method, msg, map)
115 App.log("SELECT: %s", msg)
121 method.select_file = function(method, tab)
122 App.log("SELECT FILE: %s", tab.title or "Select File")
127 -- Constructor within a constructor, here...
129 method.new_progress_bar = function(method, tab)
132 method.start = function(method)
133 App.log("START PROGRESS BAR")
137 method.set_amount = function(method, new_amount)
138 App.log("SET PROGRESS AMOUNT: %d", new_amount)
142 method.set_short_desc = function(method, new_short_desc)
143 App.log("SET PROGRESS DESC: %d", new_short_desc)
147 method.update = function(method)
148 App.log("PROGRESS UPDATE: %d", new_amount)
152 method.stop = function(method)
153 App.log("STOP PROGRESS BAR")
164 -- Begin setting up the App.
172 App.merge_tables(opt, App.defaults, function(key, dest_val, src_val)
180 -- Set name of application.
182 App.log_filename = opt.logfile
184 -- Set up directories, and make sure each ends with a slash.
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.
194 App.os.name = App.determine_os_name()
195 -- App.os.version = App.determine_os_version()
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)
212 if table.getn(ltables) > 0 then
213 for i, tab in ipairs(ltables) do
221 if table.getn(gtables) > 0 then
222 for i, tab in ipairs(gtables) do
230 App.log_warn("Could not expand `${%s}'", key)
231 return "${" .. key .. "}"
237 -- Set up temporary files.
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()
261 App.log("Shutting down")
265 App.process_cmdline = function(arg)
267 local remaining_arg = {}
270 if arg[argn] == "-C" then
272 App.add_conf_path(arg[argn])
273 elseif arg[argn] == "-L" then
275 App.add_pkg_path(arg[argn])
276 elseif arg[argn] == "-R" then
278 local script_name = App.find_script(arg[argn]) or arg[argn]
279 local ok, result = App.run(script_name)
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])
288 table.insert(remaining_arg, arg[argn])
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
306 found, len, k, v = string.find(expr, "^(.*)=(.*)$")
307 for c in string.gfind(k, "[^%.]+") do
311 if i == table.getn(r) then
317 if type(t[c]) == "table" then
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"
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
383 App.current_script = script_name
385 if App.option.fatal_errors then
389 ok, result = pcall(fun)
392 App.current_script = save_script
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.
415 -- Couldn't find it relative to the current script.
417 io.stderr:write("WARNING: could not find `" .. script_name .. "'\n")
423 -- Dump the contents of the given table to stdout,
424 -- primarily intended for debugging.
426 App.dump_table = function(tab, indent)
434 if type(v) == "table" then
435 print(indent .. tostring(k) .. "=")
436 App.dump_table(v, indent .. "\t")
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)
454 -- A 'non-overriding' merge can be accomplished with:
455 -- function(key, dest_val, src_val)
456 -- if dest_val == nil then
463 App.merge_tables = function(dest, src, fun)
467 if type(v) == "table" then
471 if type(dest[k]) == "table" then
472 App.merge_tables(dest[k], v, fun)
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))
489 App.log_warn("Error occurred while loading script `" ..
490 tostring(script_name) .. "': " .. tostring(result))
494 name = "Error Loading Script",
496 "An internal Lua error occurred while " ..
497 "trying to run the script " ..
498 tostring(script_name) .. ":\n\n" ..
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
533 local time_elapsed = 0
534 local cancelled = false
536 assert(type(predicate) == "function")
542 pr = App.ui:new_progress_bar{
544 short_desc = short_desc
548 while time_elapsed < timeout and not cancelled and not result do
549 POSIX.nanosleep(frequency)
550 time_elapsed = time_elapsed + frequency
552 return true, time_elapsed
554 pr:set_amount((time_elapsed * 100) / timeout)
555 cancelled = not pr:update()
560 return false, time_elapsed
564 -- Configuration file loading.
567 App.locate_conf = function(name)
570 for comp in string.gfind(App.conf_path, "[^;]+") do
571 comp = string.gsub(comp, "?", name)
572 if FileName.is_file(comp) then
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)
587 App.log_warn("Could not locate configuration file '%s'!", name)
596 App.open_log = function(filename, mode)
603 local fh, err = io.open(filename, mode)
612 App.close_log = function()
619 App.log = function(str, ...)
620 local stamp = math.floor(os.time())
623 local write_log = function(s)
627 App.log_file:write(s)
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))
649 App.view_log = function()
655 fh = io.open(App.dir.tmp .. App.log_filename, "r")
656 for line in fh:lines() do
657 contents = contents .. line .. "\n"
663 name = App.name .. ": Log",
664 short_desc = contents,
665 role = "informative",
666 minimum_width = "72",
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)
703 fh, filename = POSIX.mkstemp(App.dir.tmp .. "Lua.XXXXXXXX")
704 filename = FileName.basename(filename)
706 fh, err = io.open(App.dir.tmp .. filename, mode or "w+")
711 App.register_tmpfile(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()
732 -- Install logging wrappers around every method in a class/object.
734 App.log_methods = function(obj_method_table)
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)
748 -- END of lib/app.lua --