Install a moduli(5) manual page.
[dragonfly.git] / contrib / bsdinstaller-1.1.6 / src / backend / lua / lib / CmdChain.lua
1 -- $Id: CmdChain.lua,v 1.33 2005/03/26 23:36:13 cpressey Exp $
2
3 require "app"
4
5 --[[-----------]]--
6 --[[ CmdChains ]]--
7 --[[-----------]]--
8
9 -- Global "class" variable:
10 CmdChain = {}
11
12 -- Some 'symbolic constants':
13 CmdChain.LOG_SILENT             = {}
14 CmdChain.LOG_QUIET              = {}
15 CmdChain.LOG_VERBOSE            = {}
16
17 CmdChain.FAILURE_IGNORE         = {}
18 CmdChain.FAILURE_WARN           = {}
19 CmdChain.FAILURE_ABORT          = {}
20
21 CmdChain.RESULT_NEVER_EXECUTED  = {}
22 CmdChain.RESULT_POPEN_ERROR     = {}
23 CmdChain.RESULT_SELECT_ERROR    = {}
24 CmdChain.RESULT_CANCELLED       = {}
25 CmdChain.RESULT_SKIPPED         = {}
26
27 -- Create a new command chain object instance.
28 CmdChain.new = function(...)
29         local chain = {}
30         local cmds = {}
31         local capture = {}
32         local replacements = {}
33
34         --
35         -- Private functions.
36         --
37
38         -- Fix up a command descriptor.  If it is just a string, turn
39         -- it into a table; fill out any missing default values in the
40         -- table; and expand the string as appropriate.
41         local fix_cmd = function(cmd)
42                 if type(cmd) == "string" then
43                         cmd = { cmdline = cmd }
44                 end
45                 assert(type(cmd) == "table")
46
47                 if cmd.cmdline == nil then
48                         cmd.cmdline = ""
49                 end
50                 assert(type(cmd.cmdline) == "string")
51
52                 if cmd.log_mode == nil then
53                         cmd.log_mode = CmdChain.LOG_VERBOSE
54                 end
55                 assert(cmd.log_mode == CmdChain.LOG_SILENT or
56                        cmd.log_mode == CmdChain.LOG_QUIET or
57                        cmd.log_mode == CmdChain.LOG_VERBOSE)
58
59                 if cmd.failure_mode == nil then
60                         cmd.failure_mode = CmdChain.FAILURE_ABORT
61                 end
62                 assert(cmd.failure_mode == CmdChain.FAILURE_IGNORE or
63                        cmd.failure_mode == CmdChain.FAILURE_WARN or
64                        cmd.failure_mode == CmdChain.FAILURE_ABORT)
65
66                 if cmd.replacements == nil then
67                         cmd.replacements = {}
68                 end
69                 assert(type(cmd.replacements) == "table")
70
71                 cmd.cmdline = App.expand(cmd.cmdline,
72                     cmd.replacements, replacements)
73
74                 cmd.display_cmdline = cmd.cmdline
75
76                 if cmd.on_executed ~= nil then
77                         assert(type(cmd.on_executed) == "function")
78                 end
79
80                 -- cmd.desc, cmd.tag, cmd.sensitive, cmd.on_executed,
81                 -- and cmd.input are left as nil if not specified.
82
83                 return cmd
84         end
85
86         -- Open a stream to a command to be executed, read from it,
87         -- and update the progress bar as data comes and (and/or as the
88         -- read from the stream times out.)
89
90         -- Returns:
91         --   - the return value of the command on the other end of the stream;
92         --   - a boolean indicating whether it was cancelled by the user; and
93         --   - the output of the command, if requested (cmd.capture.)
94
95         local stream_loop = function(pr, cmd)
96                 local done = false
97                 local cancelled = false
98                 local pty
99                 local output = {}
100                 local cmdline
101
102                 local escape = function(pat)
103                         return string.gsub(pat, "([^%w])", "%%%1")
104                 end
105
106                 cmdline = "(" .. cmd.cmdline .. ") 2>&1"
107                 if not cmd.input then
108                         cmdline = cmdline .. " </dev/null"
109                 end
110                 pty = Pty.open(cmdline)
111                 if not pty then
112                         App.log("! could not open pty to: " .. cmd.cmdline)
113                         return CmdChain.RESULT_POPEN_ERROR, false, output
114                 end
115                 if cmd.input then
116                         pty:write(cmd.input)
117                         pty:flush()
118                 end
119
120                 while not done do
121                         if cancelled then break end
122                         line, err = pty:readline(1000)
123
124                         if line then
125                                 if cmd.sensitive ~= nil then
126                                         assert(type(cmd.sensitive) == "string")
127                                         line = string.gsub(
128                                             line, escape(cmd.sensitive), "***not*shown***"
129                                         )
130                                 end
131                                 cancelled = not pr:update()
132                                 if cmd.log_mode == CmdChain.LOG_VERBOSE then
133                                         App.log("| " .. line)
134                                 elseif cmd.log_mode == CmdChain.LOG_QUIET then
135                                         io.stderr:write("| " .. line .. "\n")
136                                 else -- cmd.log_mode == CmdChain.LOG_SILENT
137                                         -- do nothing
138                                 end
139                                 if cmd.capture then
140                                         table.insert(output, line)
141                                 end
142                         else
143                                 if err == Pty.TIMEOUT then
144                                         cancelled = not pr:update()
145                                 elseif err == Pty.EOF then
146                                         break
147                                 else
148                                         App.log("! pty:read() failed, err=%d", err)
149                                         pty:close()
150                                         return CmdChain.RESULT_SELECT_ERROR,
151                                             false, output;
152                                 end
153                         end
154                 end
155         
156                 if cancelled then
157                         pty:signal(Pty.SIGTERM)
158                 end
159
160                 return pty:close(), cancelled, output
161         end
162         
163         local interruption_dialog = function(cmd, cancelled, result)
164                 local done_interruption = false
165                 local done_command = false
166                 local msg
167                 
168                 if cancelled then
169                         msg = "was cancelled."
170                 else
171                         msg = "FAILED with a return code of " .. tostring(result) .. "."
172                 end
173                 
174                 while not done_interruption do
175                         App.ui:present({
176                             id = "cancelled",
177                             name = "Cancelled",
178                             short_desc = "Execution of the command\n\n" ..
179                                 cmd.display_cmdline .. "\n\n" .. msg,
180                             actions = {
181                                 {
182                                     id = "view_log",
183                                     name = "View Log",
184                                     short_desc = "View the command output that led up to this",
185                                     effect = function()
186                                         App.view_log()
187                                     end
188                                 },
189                                 {
190                                     id = "retry",
191                                     name = "Retry",
192                                     short_desc = "Try executing this command again",
193                                     effect = function()
194                                         done_interruption = true
195                                     end
196                                 },
197                                 {
198                                     id = "cancel",
199                                     name = "Cancel",
200                                     short_desc = "Abort this sequence of commands",
201                                     effect = function()
202                                         result = CmdChain.RESULT_CANCELLED
203                                         done_interruption = true
204                                         done_command = true
205                                     end
206                                 },
207                                 {
208                                     id = "skip",
209                                     name = "Skip",
210                                     short_desc = "Skip this particular command and resume with the next one",
211                                     effect = function()
212                                         result = CmdChain.RESULT_SKIPPED
213                                         done_interruption = true
214                                         done_command = true
215                                     end
216                                 }
217                             }
218                         })
219                 end
220         
221                 return done_command, result
222         end
223         
224         -- Execute a single command.
225         -- Return values are:
226         --  - a COMMAND_RESULT_* constant, or a value from 0 to 255
227         --    to indicate the exit code from the utility; and
228         --  - the output of the command, if requested (cmd.capture.)
229         
230         local command_execute = function(pr, cmd)
231                 local filename
232                 local cancelled = false
233                 local done_command = false
234                 local result, output = CmdChain.RESULT_NEVER_EXECUTED, ""
235         
236                 if cmd.desc then
237                         pr:set_short_desc(cmd.desc)
238                 else
239                         pr:set_short_desc(cmd.display_cmdline)
240                 end
241                 cancelled = not pr:update()
242         
243                 if App.option.confirm_execution then
244                         done_command = not App.ui:confirm(
245                             "About to execute:\n\n" .. cmd.display_cmdline ..
246                             "\n\nIs this acceptable?"
247                         )
248                 end
249         
250                 while not done_command do
251                         output = nil
252                         if cmd.log_mode ~= CmdChain.LOG_SILENT then
253                                 App.log(",-<<< Executing `" .. cmd.display_cmdline .. "'")
254                         end
255                         if App.option.fake_execution then
256                                 if cmd.log_mode ~= CmdChain.LOG_SILENT then
257                                         App.log("| (not actually executed)")
258                                 end
259                                 result = 0
260                         else
261                                 result, cancelled, output = stream_loop(pr, cmd)
262                         end
263                         if cmd.log_mode ~= CmdChain.LOG_SILENT then
264                                 App.log("`->>> Exit status: " .. tostring(result))
265                         end
266         
267                         if cancelled then
268                                 pr:stop()
269                                 done_command, result = interruption_dialog(cmd, cancelled, result)
270                                 pr:start()
271                         elseif cmd.failure_mode == CmdChain.FAILURE_IGNORE then
272                                 result = 0
273                                 done_command = true
274                         elseif (result ~= 0 and cmd.failure_mode ~= CmdChain.FAILURE_WARN) then
275                                 pr:stop()
276                                 done_command, result = interruption_dialog(cmd, cancelled, result)
277                                 pr:start()
278                         else
279                                 done_command = true
280                         end
281                 end
282                 if cmd.on_executed ~= nil then
283                         cmd.on_executed(cmd, result, output)
284                 end
285         
286                 return result, output
287         end
288
289         --
290         -- Methods.
291         --
292
293         --
294         -- Set the global replacements for this command chain.
295         -- These will be applied to each command that is
296         -- subsequently added (although local replacements will
297         -- be applied first.)
298         --
299         chain.set_replacements = function(cmd, new_replacements)
300                 App.merge_tables(replacements, new_replacements,
301                     function(key, dest_val, src_val)
302                         return src_val
303                     end)
304         end
305
306         --
307         -- Get captured output by its id.
308         --
309         chain.get_output = function(cmd, cap_id)
310                 return capture[cap_id]
311         end
312
313         --
314         -- Add one or more commands to this command chain.
315         --
316         chain.add = function(chain, ...)
317                 local cmd_no, cmd
318
319                 if table.getn(arg) == 0 then
320                         return
321                 end
322
323                 for cmd_no, cmd in ipairs(arg) do
324                         table.insert(cmds, fix_cmd(cmd))
325                 end
326         end
327
328         -- Execute a series of external utility programs.
329         -- Returns 1 if everything executed OK, 0 if one of the
330         -- critical commands failed or if the user cancelled.
331         chain.execute = function(chain)
332                 local pr
333                 local cmd
334                 local i, n, result = 0, 0, 0
335                 local return_val = true
336                 local output
337
338                 n = table.getn(cmds)
339         
340                 pr = App.ui:new_progress_bar{
341                     title = "Executing Commands"
342                 }
343
344                 pr:start()
345         
346                 for i in cmds do
347                         result, output = command_execute(pr, cmds[i])
348                         if result == CmdChain.RESULT_CANCELLED then
349                                 return_val = false
350                                 break
351                         end
352                         if type(result) == "number" and result > 0 and result < 256 then
353                                 return_val = false
354                                 if cmd.failure_mode == CmdChain.FAILURE_ABORT then
355                                         break
356                                 end
357                         end
358                         if cmds[i].capture then
359                                 capture[cmds[i].capture] = output
360                         end
361                         pr:set_amount((i * 100) / n)
362                 end
363         
364                 pr:stop()
365         
366                 return return_val
367         end
368
369         -- Show the commands that have been added to this
370         -- command chain to the user in a dialog box.
371         chain.preview = function(chain)
372                 local contents = ""
373                 local i, cmd
374         
375                 for i, cmd in cmds do
376                         contents = contents .. cmd.cmdline .. "\n"
377                 end
378         
379                 App.ui:present({
380                         id = "cmd_preview",
381                         name = "Command Preview",
382                         short_desc = contents,
383                         role = "informative",
384                         minimum_width = "72",
385                         monospaced = "true",
386                         actions = {
387                                 { id = "ok", name = "OK" }
388                         }
389                 })
390         end
391
392         -- Record these commands in a shell script file.
393         chain.record = function(chain, file)
394                 local contents = ""
395                 local i, cmd
396
397                 local gen_rand_string = function(len)
398                         local n = 1
399                         local s = ""
400                         local A = string.byte("A")
401                         assert(len >= 0)
402
403                         while n <= len do
404                                 s = s .. string.char(math.random(A, A + 25))
405                                 n = n + 1
406                         end
407
408                         return s
409                 end
410
411                 local get_marker_for = function(text)
412                         local s
413
414                         s = gen_rand_string(5)
415                         while string.find(text, s, 1, true) do
416                                 s = gen_rand_string(5)
417                         end
418                         return s
419                 end
420
421                 for i, cmd in cmds do
422                         --
423                         -- Write lines apropos to the cmdline being
424                         -- executed, taking in account the failure mode.
425                         --
426                         if cmd.failure_mode == CmdChain.FAILURE_IGNORE then
427                                 file:write(cmd.cmdline)
428                         elseif cmd.failure_mode == CmdChain.FAILURE_WARN then
429                                 file:write("(" .. cmd.cmdline .. " || " ..
430                                    "echo \"WARNING: " .. cmd.cmdline ..
431                                    " failed with exit code $?\")")
432                         elseif cmd.failure_mode == CmdChain.FAILURE_ABORT then
433                                 file:write(cmd.cmdline)
434                         end
435
436                         --
437                         -- If the command has input, include that as a heredoc.
438                         
439                         if cmd.input then
440                                 local marker = get_marker_for(cmd.input)
441                                 file:write(" <<" .. marker .. "\n" .. cmd.input .. marker)
442                         end
443
444                         if cmd.failure_mode == CmdChain.FAILURE_WARN or
445                            cmd.failure_mode == CmdChain.FAILURE_ABORT then
446                                 file:write(" && \\")
447                         end
448
449                         --
450                         -- Write the description as a comment following.
451                         --
452                         if cmd.desc then
453                                 file:write("   # " .. cmd.desc)
454                         end
455
456                         file:write("\n")
457                 end
458         end
459
460         -- ``Constructor'' - initialize our instance data.
461
462         if table.getn(arg) > 0 then
463                 chain:add(unpack(arg))
464         end
465
466         return chain
467 end