Installer import into contrib (real import this time)
[dragonfly.git] / contrib / bsdinstaller-1.1.6 / src / lib / lua / app / app.lua
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.
4
5 -- BEGIN app.lua --
6
7 module("app")
8
9 local POSIX = require("posix")
10 local FileName = require("filename")
11 local Pty = require("pty")
12
13 --[[-----]]--
14 --[[ App ]]--
15 --[[-----]]--
16
17 -- Application Environment - roughly equivalent to
18 -- InstallerContext (or i_fn_args in the C version,) but:
19 --
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:
23 --
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
33 --
34 -- For simplicity, we consider this to be a singleton or
35 -- "static object" (with a single global "instance" called App.)
36
37 App = {}
38
39 --
40 -- Initialize global stuff.
41 --
42
43 App.init = function()
44         App.defaults = {
45             name        = "Unnamed Application",
46             logfile     = "unnamed.log",
47             dir = {
48                 root    = "/",
49                 tmp     = "/tmp/"
50             },
51             transport   = "tcp",
52             rendezvous  = "9999"
53         }
54         
55         App.last_log_time = -1
56         App.conf_path = ""
57         App.current_script = arg[0]
58         
59         App.config = {}
60         App.option = {}
61         App.state = {}
62
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")
67
68         arg = App.process_cmdline(arg)
69 end
70
71 --
72 -- Startup and shutdown.
73 --
74
75 App.start = function(opt)
76         local k, v
77
78         --
79         -- Private function to create a dummy user interface adapter
80         -- if the App was started without one.
81         --
82         local new_dummy_ui = function()
83                 local method = {}
84         
85                 method.start = function(method)
86                         App.log("Dummy user interface started")
87                         return true
88                 end
89         
90                 method.stop = function(method)
91                         App.log("Dummy user interface stopped")
92                         return true
93                 end
94         
95                 method.present = function(method, tab)
96                         App.dump_table(tab)
97                         return {
98                             action_id = tab.actions[1].id,
99                             datasets = tab.datasets
100                         }
101                 end
102         
103                 method.inform = function(method, msg)
104                         App.log("INFORM: %s", msg)
105                         return { action_id = "ok", datasets = {} }
106                 end
107                 
108                 method.confirm = function(method, msg)
109                         App.log("CONFIRM: %s", msg)
110                         return true
111                 end
112         
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
118                         end
119                 end
120         
121                 method.select_file = function(method, tab)
122                         App.log("SELECT FILE: %s", tab.title or "Select File")
123                         return "cancel"
124                 end
125         
126                 --
127                 -- Constructor within a constructor, here...
128                 --
129                 method.new_progress_bar = function(method, tab)
130                         local method = {}
131         
132                         method.start = function(method)
133                                 App.log("START PROGRESS BAR")
134                                 return true
135                         end
136         
137                         method.set_amount = function(method, new_amount)
138                                 App.log("SET PROGRESS AMOUNT: %d", new_amount)
139                                 return true
140                         end
141         
142                         method.set_short_desc = function(method, new_short_desc)
143                                 App.log("SET PROGRESS DESC: %d", new_short_desc)
144                                 return true
145                         end
146         
147                         method.update = function(method)
148                                 App.log("PROGRESS UPDATE: %d", new_amount)
149                                 return true
150                         end
151         
152                         method.stop = function(method)
153                                 App.log("STOP PROGRESS BAR")
154                                 return true
155                         end
156         
157                         return method
158                 end
159         
160                 return method
161         end
162
163         --
164         -- Begin setting up the App.
165         --
166
167         -- Set up defaults.
168         if not opt then
169                 opt = {}
170         end
171
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
177                 end
178         end)
179
180         -- Set name of application.
181         App.name = opt.name
182         App.log_filename = opt.logfile
183
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 .. "/"
189                 end
190         end
191
192         -- Determine the operating system.
193         App.os = {}
194         App.os.name = App.determine_os_name()
195         -- App.os.version = App.determine_os_version()
196
197         -- Open our logfile.
198         App.open_log(App.dir.tmp .. App.log_filename)
199         App.log(App.name .. " started")
200
201         -- Load command names, if available.
202         App.cmd_names = App.load_conf("cmdnames")
203
204         -- Set up the ${}-expansion function.
205         App.expand = function(str, ...)
206                 local ltables = arg or {}
207                 local gtables = {App.cmd_names, App.dir}
208
209                 local result = string.gsub(str, "%$%{([%w_]+)%}", function(key)
210                         local i, tab, value
211
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
217                                         end
218                                 end
219                         end
220
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
226                                         end
227                                 end
228                         end
229
230                         App.log_warn("Could not expand `${%s}'", key)
231                         return "${" .. key .. "}"
232                 end)
233
234                 return result
235         end
236
237         -- Set up temporary files.
238         App.tmpfile = {}
239         
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
247
248         -- Seed the random-number generator.
249         math.randomseed(os.time())
250
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")
255         end
256 end
257
258 App.stop = function()
259         App.clean_tmpfiles()
260         App.ui:stop()
261         App.log("Shutting down")
262         App.close_log()
263 end
264
265 App.process_cmdline = function(arg)
266         local argn = 1
267         local remaining_arg = {}
268
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")
284                         end
285                 elseif string.find(arg[argn], "=") then
286                         App.set_property(arg[argn])
287                 else
288                         table.insert(remaining_arg, arg[argn])
289                 end
290
291                 argn = argn + 1
292         end
293
294         return remaining_arg
295 end
296
297 --
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".
300 --
301 App.set_property = function(expr)
302         local found, len, k, v, c, r, i, t
303
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)
309         end
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] = {}
316                         end
317                         if type(t[c]) == "table" then
318                                 t = t[c]
319                         else
320                                 App.log_warn("%s: not a table", tostring(c))
321                         end
322                 end
323         end
324 end
325
326 --
327 -- Add a directory to package.path (used by compat-5.1.)
328 --
329 App.add_pkg_path = function(dir)
330         if package and package.path then
331                 if package.path ~= "" then
332                         package.path = package.path .. ";"
333                 end
334                 package.path = package.path .. tostring(dir) .. "/?.lua"
335         end
336 end
337
338 --
339 -- Add a directory to App.conf_path (used by App.load_conf().)
340 --
341 App.add_conf_path = function(dir)
342         if App.conf_path ~= "" then
343                 App.conf_path = App.conf_path .. ";"
344         end
345         App.conf_path = App.conf_path .. tostring(dir) .. "/?.lua"
346 end
347
348 --
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.
357 --
358 App.run = function(script_name, ...)
359         local save_script = App.current_script
360         local save_args = ARG
361         local ok, result, fun, errmsg
362
363         if App.option.fatal_errors then
364                 assert(script_name and type(script_name) == "string",
365                        "bad filename " .. tostring(script_name))
366         end
367         if not script_name or type(script_name) ~= "string" then
368                 return false, "bad filename " .. tostring(script_name)
369         end
370
371         App.add_pkg_path(FileName.dirname(script_name) .. "lib")
372         App.add_conf_path(FileName.dirname(script_name) .. "conf")
373
374         fun, errmsg = loadfile(script_name)
375
376         if App.option.fatal_errors then
377                 assert(fun, errmsg)
378         end
379         if not fun then
380                 return false, errmsg
381         end
382
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)
390         end
391         ARG = save_args
392         App.current_script = save_script
393
394         return ok, result
395 end
396
397 --
398 -- Find a Lua script.
399 --
400 App.find_script = function(script_name)
401         script_name = FileName.dirname(App.current_script) .. script_name
402
403         if FileName.is_dir(script_name) then
404                 if string.sub(script_name, -1, -1) ~= "/" then
405                         script_name = script_name .. "/"
406                 end
407                 return script_name .. "main.lua"
408         elseif FileName.is_file(script_name) then
409                 --
410                 -- Just execute that script.
411                 --
412                 return script_name
413         else
414                 --
415                 -- Couldn't find it relative to the current script.
416                 --
417                 io.stderr:write("WARNING: could not find `" .. script_name .. "'\n")
418                 return nil
419         end
420 end
421
422 --
423 -- Dump the contents of the given table to stdout,
424 -- primarily intended for debugging.
425 --
426 App.dump_table = function(tab, indent)
427         local k, v
428
429         if not indent then
430                 indent = ""
431         end
432
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))
439                 end
440         end
441 end
442
443 --
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.
448 --
449 -- An 'overriding' merge can be accomplished with:
450 --      function(key, dest_val, src_val)
451 --              return src_val
452 --      end
453 --
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
462 --
463 App.merge_tables = function(dest, src, fun)
464         local k, v
465
466         for k, v in src do
467                 if type(v) == "table" then
468                         if not dest[k] then
469                                 dest[k] = {}
470                         end
471                         if type(dest[k]) == "table" then
472                                 App.merge_tables(dest[k], v, fun)
473                         end
474                 else
475                         dest[k] = fun(k, dest[k], v)
476                 end
477         end
478 end
479
480 --
481 -- Run a script.  Expects the full filename (will not search.)
482 -- Displays a nice dialog box if the script contained errors.
483 --
484 App.run_script = function(script_name, ...)
485         local ok, result = App.run(script_name, unpack(arg))
486         if ok then
487                 return result
488         end
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 = {
502                         {
503                             id = "ok",
504                             name = "OK"
505                         }
506                     }
507                 }
508         end
509         return nil
510 end
511
512 --
513 -- Run a sub-application (a script relative to the current script.)
514 --
515 App.descend = function(script_name, ...)
516         return App.run_script(App.find_script(script_name), unpack(arg))
517 end
518
519 --
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.)
525 --
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
535
536         assert(type(predicate) == "function")
537
538         if predicate() then
539                 return true
540         end
541
542         pr = App.ui:new_progress_bar{
543             title = title,
544             short_desc = short_desc
545         }
546         pr:start()
547         
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
553                 end
554                 pr:set_amount((time_elapsed * 100) / timeout)
555                 cancelled = not pr:update()
556         end
557
558         pr:stop()
559
560         return false, time_elapsed
561 end
562
563 --
564 -- Configuration file loading.
565 --
566
567 App.locate_conf = function(name)
568         local comp
569
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
574                 end
575         end
576         
577         return nil
578 end
579
580 App.load_conf = function(name)
581         local filename = App.locate_conf(name)
582
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
589         end
590 end
591
592 --
593 -- Logging.
594 --
595
596 App.open_log = function(filename, mode)
597         if App.log_file then
598                 return
599         end
600         if not mode then
601                 mode = "w"
602         end
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)
609         end
610 end
611
612 App.close_log = function()
613         if App.log_file then
614                 App.log_file:close()
615                 App.log_file = nil
616         end
617 end
618
619 App.log = function(str, ...)
620         local stamp = math.floor(os.time())
621         local line = ""
622
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()
629                 end
630         end
631
632         if stamp > App.last_log_time then
633                 App.last_log_time = stamp
634                 write_log("[" .. os.date() .. "]")
635         end
636
637         write_log(string.format(str, unpack(arg)))
638 end
639
640 App.log_warn = function(str, ...)
641         App.log("WARNING: " .. str, unpack(arg))
642 end
643
644 App.log_fatal = function(str, ...)
645         App.log(str, unpack(arg))
646         error(str)
647 end
648
649 App.view_log = function()
650         local contents = ""
651         local fh
652
653         App.close_log()
654
655         fh = io.open(App.dir.tmp .. App.log_filename, "r")
656         for line in fh:lines() do
657                 contents = contents .. line .. "\n"
658         end
659         fh:close()
660
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" }
670                 }
671         })
672         
673         App.open_log(App.dir.tmp .. App.log_filename, "a")
674 end
675
676 --
677 -- Temporary file handling.
678 --
679
680 App.clean_tmpfiles = function()
681         local filename, unused
682
683         for filename, unused in App.tmpfile do
684                 App.log("Deleting tmpfile: " .. filename)
685                 os.remove(App.dir.tmp .. filename)
686         end
687 end
688
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
693 end
694
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
701
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
709                 end
710         end
711         App.register_tmpfile(filename)
712         return fh, filename
713 end
714
715 --
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.
721 --
722
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
728 end
729
730 --
731 -- More debugging.
732 -- Install logging wrappers around every method in a class/object.
733 --
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)
742                 end
743         end
744 end
745
746 return App
747
748 -- END of lib/app.lua --