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 --