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