]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - stand/lua/menu.lua
zfs: add an option to the bootloader to rewind the ZFS checkpoint
[FreeBSD/FreeBSD.git] / stand / lua / menu.lua
1 --
2 -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3 --
4 -- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org>
5 -- Copyright (c) 2018 Kyle Evans <kevans@FreeBSD.org>
6 -- All rights reserved.
7 --
8 -- Redistribution and use in source and binary forms, with or without
9 -- modification, are permitted provided that the following conditions
10 -- are met:
11 -- 1. Redistributions of source code must retain the above copyright
12 --    notice, this list of conditions and the following disclaimer.
13 -- 2. Redistributions in binary form must reproduce the above copyright
14 --    notice, this list of conditions and the following disclaimer in the
15 --    documentation and/or other materials provided with the distribution.
16 --
17 -- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 -- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 -- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 -- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 -- SUCH DAMAGE.
28 --
29 -- $FreeBSD$
30 --
31
32 local cli = require("cli")
33 local core = require("core")
34 local color = require("color")
35 local config = require("config")
36 local screen = require("screen")
37 local drawer = require("drawer")
38
39 local menu = {}
40
41 local drawn_menu
42 local return_menu_entry = {
43         entry_type = core.MENU_RETURN,
44         name = "Back to main menu" .. color.highlight(" [Backspace]"),
45 }
46
47 local function OnOff(str, value)
48         if value then
49                 return str .. color.escapefg(color.GREEN) .. "On" ..
50                     color.resetfg()
51         else
52                 return str .. color.escapefg(color.RED) .. "off" ..
53                     color.resetfg()
54         end
55 end
56
57 local function bootenvSet(env)
58         loader.setenv("vfs.root.mountfrom", env)
59         loader.setenv("currdev", env .. ":")
60         config.reload()
61 end
62
63 -- Module exports
64 menu.handlers = {
65         -- Menu handlers take the current menu and selected entry as parameters,
66         -- and should return a boolean indicating whether execution should
67         -- continue or not. The return value may be omitted if this entry should
68         -- have no bearing on whether we continue or not, indicating that we
69         -- should just continue after execution.
70         [core.MENU_ENTRY] = function(_, entry)
71                 -- run function
72                 entry.func()
73         end,
74         [core.MENU_CAROUSEL_ENTRY] = function(_, entry)
75                 -- carousel (rotating) functionality
76                 local carid = entry.carousel_id
77                 local caridx = config.getCarouselIndex(carid)
78                 local choices = entry.items
79                 if type(choices) == "function" then
80                         choices = choices()
81                 end
82                 if #choices > 0 then
83                         caridx = (caridx % #choices) + 1
84                         config.setCarouselIndex(carid, caridx)
85                         entry.func(caridx, choices[caridx], choices)
86                 end
87         end,
88         [core.MENU_SUBMENU] = function(_, entry)
89                 menu.process(entry.submenu)
90         end,
91         [core.MENU_RETURN] = function(_, entry)
92                 -- allow entry to have a function/side effect
93                 if entry.func ~= nil then
94                         entry.func()
95                 end
96                 return false
97         end,
98 }
99 -- loader menu tree is rooted at menu.welcome
100
101 menu.boot_environments = {
102         entries = {
103                 -- return to welcome menu
104                 return_menu_entry,
105                 {
106                         entry_type = core.MENU_CAROUSEL_ENTRY,
107                         carousel_id = "be_active",
108                         items = core.bootenvList,
109                         name = function(idx, choice, all_choices)
110                                 if #all_choices == 0 then
111                                         return "Active: "
112                                 end
113
114                                 local is_default = (idx == 1)
115                                 local bootenv_name = ""
116                                 local name_color
117                                 if is_default then
118                                         name_color = color.escapefg(color.GREEN)
119                                 else
120                                         name_color = color.escapefg(color.BLUE)
121                                 end
122                                 bootenv_name = bootenv_name .. name_color ..
123                                     choice .. color.resetfg()
124                                 return color.highlight("A").."ctive: " ..
125                                     bootenv_name .. " (" .. idx .. " of " ..
126                                     #all_choices .. ")"
127                         end,
128                         func = function(_, choice, _)
129                                 bootenvSet(choice)
130                         end,
131                         alias = {"a", "A"},
132                 },
133                 {
134                         entry_type = core.MENU_ENTRY,
135                         visible = function()
136                                 return core.isRewinded() == false
137                         end,
138                         name = function()
139                                 return color.highlight("b") .. "ootfs: " ..
140                                     core.bootenvDefault()
141                         end,
142                         func = function()
143                                 -- Reset active boot environment to the default
144                                 config.setCarouselIndex("be_active", 1)
145                                 bootenvSet(core.bootenvDefault())
146                         end,
147                         alias = {"b", "B"},
148                 },
149         },
150 }
151
152 menu.boot_options = {
153         entries = {
154                 -- return to welcome menu
155                 return_menu_entry,
156                 -- load defaults
157                 {
158                         entry_type = core.MENU_ENTRY,
159                         name = "Load System " .. color.highlight("D") ..
160                             "efaults",
161                         func = core.setDefaults,
162                         alias = {"d", "D"},
163                 },
164                 {
165                         entry_type = core.MENU_SEPARATOR,
166                 },
167                 {
168                         entry_type = core.MENU_SEPARATOR,
169                         name = "Boot Options:",
170                 },
171                 -- acpi
172                 {
173                         entry_type = core.MENU_ENTRY,
174                         visible = core.isSystem386,
175                         name = function()
176                                 return OnOff(color.highlight("A") ..
177                                     "CPI       :", core.acpi)
178                         end,
179                         func = core.setACPI,
180                         alias = {"a", "A"},
181                 },
182                 -- safe mode
183                 {
184                         entry_type = core.MENU_ENTRY,
185                         name = function()
186                                 return OnOff("Safe " .. color.highlight("M") ..
187                                     "ode  :", core.sm)
188                         end,
189                         func = core.setSafeMode,
190                         alias = {"m", "M"},
191                 },
192                 -- single user
193                 {
194                         entry_type = core.MENU_ENTRY,
195                         name = function()
196                                 return OnOff(color.highlight("S") ..
197                                     "ingle user:", core.su)
198                         end,
199                         func = core.setSingleUser,
200                         alias = {"s", "S"},
201                 },
202                 -- verbose boot
203                 {
204                         entry_type = core.MENU_ENTRY,
205                         name = function()
206                                 return OnOff(color.highlight("V") ..
207                                     "erbose    :", core.verbose)
208                         end,
209                         func = core.setVerbose,
210                         alias = {"v", "V"},
211                 },
212         },
213 }
214
215 menu.welcome = {
216         entries = function()
217                 local menu_entries = menu.welcome.all_entries
218                 local multi_user = menu_entries.multi_user
219                 local single_user = menu_entries.single_user
220                 local boot_entry_1, boot_entry_2
221                 if core.isSingleUserBoot() then
222                         -- Swap the first two menu items on single user boot.
223                         -- We'll cache the alternate entries for performance.
224                         local alts = menu_entries.alts
225                         if alts == nil then
226                                 single_user = core.deepCopyTable(single_user)
227                                 multi_user = core.deepCopyTable(multi_user)
228                                 single_user.name = single_user.alternate_name
229                                 multi_user.name = multi_user.alternate_name
230                                 menu_entries.alts = {
231                                         single_user = single_user,
232                                         multi_user = multi_user,
233                                 }
234                         else
235                                 single_user = alts.single_user 
236                                 multi_user = alts.multi_user
237                         end
238                         boot_entry_1, boot_entry_2 = single_user, multi_user
239                 else
240                         boot_entry_1, boot_entry_2 = multi_user, single_user
241                 end
242                 return {
243                         boot_entry_1,
244                         boot_entry_2,
245                         menu_entries.prompt,
246                         menu_entries.reboot,
247                         {
248                                 entry_type = core.MENU_SEPARATOR,
249                         },
250                         {
251                                 entry_type = core.MENU_SEPARATOR,
252                                 name = "Options:",
253                         },
254                         menu_entries.kernel_options,
255                         menu_entries.boot_options,
256                         menu_entries.zpool_checkpoints,
257                         menu_entries.boot_envs,
258                         menu_entries.chainload,
259                 }
260         end,
261         all_entries = {
262                 multi_user = {
263                         entry_type = core.MENU_ENTRY,
264                         name = color.highlight("B") .. "oot Multi user " ..
265                             color.highlight("[Enter]"),
266                         -- Not a standard menu entry function!
267                         alternate_name = color.highlight("B") ..
268                             "oot Multi user",
269                         func = function()
270                                 core.setSingleUser(false)
271                                 core.boot()
272                         end,
273                         alias = {"b", "B"},
274                 },
275                 single_user = {
276                         entry_type = core.MENU_ENTRY,
277                         name = "Boot " .. color.highlight("S") .. "ingle user",
278                         -- Not a standard menu entry function!
279                         alternate_name = "Boot " .. color.highlight("S") ..
280                             "ingle user " .. color.highlight("[Enter]"),
281                         func = function()
282                                 core.setSingleUser(true)
283                                 core.boot()
284                         end,
285                         alias = {"s", "S"},
286                 },
287                 prompt = {
288                         entry_type = core.MENU_RETURN,
289                         name = color.highlight("Esc") .. "ape to loader prompt",
290                         func = function()
291                                 loader.setenv("autoboot_delay", "NO")
292                         end,
293                         alias = {core.KEYSTR_ESCAPE},
294                 },
295                 reboot = {
296                         entry_type = core.MENU_ENTRY,
297                         name = color.highlight("R") .. "eboot",
298                         func = function()
299                                 loader.perform("reboot")
300                         end,
301                         alias = {"r", "R"},
302                 },
303                 kernel_options = {
304                         entry_type = core.MENU_CAROUSEL_ENTRY,
305                         carousel_id = "kernel",
306                         items = core.kernelList,
307                         name = function(idx, choice, all_choices)
308                                 if #all_choices == 0 then
309                                         return "Kernel: "
310                                 end
311
312                                 local is_default = (idx == 1)
313                                 local kernel_name = ""
314                                 local name_color
315                                 if is_default then
316                                         name_color = color.escapefg(color.GREEN)
317                                         kernel_name = "default/"
318                                 else
319                                         name_color = color.escapefg(color.BLUE)
320                                 end
321                                 kernel_name = kernel_name .. name_color ..
322                                     choice .. color.resetfg()
323                                 return color.highlight("K") .. "ernel: " ..
324                                     kernel_name .. " (" .. idx .. " of " ..
325                                     #all_choices .. ")"
326                         end,
327                         func = function(_, choice, _)
328                                 if loader.getenv("kernelname") ~= nil then
329                                         loader.perform("unload")
330                                 end
331                                 config.selectKernel(choice)
332                         end,
333                         alias = {"k", "K"},
334                 },
335                 boot_options = {
336                         entry_type = core.MENU_SUBMENU,
337                         name = "Boot " .. color.highlight("O") .. "ptions",
338                         submenu = menu.boot_options,
339                         alias = {"o", "O"},
340                 },
341                 zpool_checkpoints = {
342                         entry_type = core.MENU_ENTRY,
343                         name = function()
344                                 rewind = "No"
345                                 if core.isRewinded() then
346                                         rewind = "Yes"
347                                 end
348                                 return "Rewind ZFS " .. color.highlight("C") ..
349                                         "heckpoint: " .. rewind
350                         end,
351                         func = function()
352                                 core.changeRewindCheckpoint()
353                                 if core.isRewinded() then
354                                         bootenvSet(
355                                             core.bootenvDefaultRewinded())
356                                 else
357                                         bootenvSet(core.bootenvDefault())
358                                 end
359                                 config.setCarouselIndex("be_active", 1)
360                         end,
361                         visible = function()
362                                 return core.isZFSBoot() and
363                                     core.isCheckpointed()
364                         end,
365                         alias = {"c", "C"},
366                 },
367                 boot_envs = {
368                         entry_type = core.MENU_SUBMENU,
369                         visible = function()
370                                 return core.isZFSBoot() and
371                                     #core.bootenvList() > 1
372                         end,
373                         name = "Boot " .. color.highlight("E") .. "nvironments",
374                         submenu = menu.boot_environments,
375                         alias = {"e", "E"},
376                 },
377                 chainload = {
378                         entry_type = core.MENU_ENTRY,
379                         name = function()
380                                 return 'Chain' .. color.highlight("L") ..
381                                     "oad " .. loader.getenv('chain_disk')
382                         end,
383                         func = function()
384                                 loader.perform("chain " ..
385                                     loader.getenv('chain_disk'))
386                         end,
387                         visible = function()
388                                 return loader.getenv('chain_disk') ~= nil
389                         end,
390                         alias = {"l", "L"},
391                 },
392         },
393 }
394
395 menu.default = menu.welcome
396 -- current_alias_table will be used to keep our alias table consistent across
397 -- screen redraws, instead of relying on whatever triggered the redraw to update
398 -- the local alias_table in menu.process.
399 menu.current_alias_table = {}
400
401 function menu.draw(menudef)
402         -- Clear the screen, reset the cursor, then draw
403         screen.clear()
404         menu.current_alias_table = drawer.drawscreen(menudef)
405         drawn_menu = menudef
406         screen.defcursor()
407 end
408
409 -- 'keypress' allows the caller to indicate that a key has been pressed that we
410 -- should process as our initial input.
411 function menu.process(menudef, keypress)
412         assert(menudef ~= nil)
413
414         if drawn_menu ~= menudef then
415                 menu.draw(menudef)
416         end
417
418         while true do
419                 local key = keypress or io.getchar()
420                 keypress = nil
421
422                 -- Special key behaviors
423                 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and
424                     menudef ~= menu.default then
425                         break
426                 elseif key == core.KEY_ENTER then
427                         core.boot()
428                         -- Should not return.  If it does, escape menu handling
429                         -- and drop to loader prompt.
430                         return false
431                 end
432
433                 key = string.char(key)
434                 -- check to see if key is an alias
435                 local sel_entry = nil
436                 for k, v in pairs(menu.current_alias_table) do
437                         if key == k then
438                                 sel_entry = v
439                                 break
440                         end
441                 end
442
443                 -- if we have an alias do the assigned action:
444                 if sel_entry ~= nil then
445                         local handler = menu.handlers[sel_entry.entry_type]
446                         assert(handler ~= nil)
447                         -- The handler's return value indicates if we
448                         -- need to exit this menu.  An omitted or true
449                         -- return value means to continue.
450                         if handler(menudef, sel_entry) == false then
451                                 return
452                         end
453                         -- If we got an alias key the screen is out of date...
454                         -- redraw it.
455                         menu.draw(menudef)
456                 end
457         end
458 end
459
460 function menu.run()
461         local autoboot_key
462         local delay = loader.getenv("autoboot_delay")
463
464         if delay ~= nil and delay:lower() == "no" then
465                 delay = nil
466         else
467                 delay = tonumber(delay) or 10
468         end
469
470         if delay == -1 then
471                 core.boot()
472                 return
473         end
474
475         menu.draw(menu.default)
476
477         if delay ~= nil then
478                 autoboot_key = menu.autoboot(delay)
479
480                 -- autoboot_key should return the key pressed.  It will only
481                 -- return nil if we hit the timeout and executed the timeout
482                 -- command.  Bail out.
483                 if autoboot_key == nil then
484                         return
485                 end
486         end
487
488         menu.process(menu.default, autoboot_key)
489         drawn_menu = nil
490
491         screen.defcursor()
492         print("Exiting menu!")
493 end
494
495 function menu.autoboot(delay)
496         local x = loader.getenv("loader_menu_timeout_x") or 4
497         local y = loader.getenv("loader_menu_timeout_y") or 23
498         local endtime = loader.time() + delay
499         local time
500         local last
501         repeat
502                 time = endtime - loader.time()
503                 if last == nil or last ~= time then
504                         last = time
505                         screen.setcursor(x, y)
506                         print("Autoboot in " .. time ..
507                             " seconds, hit [Enter] to boot" ..
508                             " or any other key to stop     ")
509                         screen.defcursor()
510                 end
511                 if io.ischar() then
512                         local ch = io.getchar()
513                         if ch == core.KEY_ENTER then
514                                 break
515                         else
516                                 -- erase autoboot msg
517                                 screen.setcursor(0, y)
518                                 print(string.rep(" ", 80))
519                                 screen.defcursor()
520                                 return ch
521                         end
522                 end
523
524                 loader.delay(50000)
525         until time <= 0
526
527         local cmd = loader.getenv("menu_timeout_command") or "boot"
528         cli_execute_unparsed(cmd)
529         return nil
530 end
531
532 -- CLI commands
533 function cli.menu()
534         menu.run()
535 end
536
537 return menu