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.CYAN) 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 boot_entry_1, 249 boot_entry_2, 250 menu_entries.prompt, 251 menu_entries.reboot, 252 menu_entries.console, 253 { 254 entry_type = core.MENU_SEPARATOR, 255 }, 256 { 257 entry_type = core.MENU_SEPARATOR, 258 name = "Kernel:", 259 }, 260 menu_entries.kernel_options, 261 { 262 entry_type = core.MENU_SEPARATOR, 263 }, 264 { 265 entry_type = core.MENU_SEPARATOR, 266 name = "Options:", 267 }, 268 menu_entries.boot_options, 269 menu_entries.zpool_checkpoints, 270 menu_entries.boot_envs, 271 menu_entries.chainload, 272 menu_entries.vendor, 273 { 274 entry_type = core.MENU_SEPARATOR, 275 }, 276 menu_entries.loader_needs_upgrade, 277 } 278 end, 279 all_entries = { 280 multi_user = { 281 entry_type = core.MENU_ENTRY, 282 name = function() 283 return color.highlight("B") .. "oot " .. 284 multiUserPrompt() .. " " .. 285 color.highlight("[Enter]") 286 end, 287 -- Not a standard menu entry function! 288 alternate_name = function() 289 return color.highlight("B") .. "oot " .. 290 multiUserPrompt() 291 end, 292 func = function() 293 core.setSingleUser(false) 294 core.boot() 295 end, 296 alias = {"b", "B"}, 297 }, 298 single_user = { 299 entry_type = core.MENU_ENTRY, 300 name = "Boot " .. color.highlight("S") .. "ingle user", 301 -- Not a standard menu entry function! 302 alternate_name = "Boot " .. color.highlight("S") .. 303 "ingle user " .. color.highlight("[Enter]"), 304 func = function() 305 core.setSingleUser(true) 306 core.boot() 307 end, 308 alias = {"s", "S"}, 309 }, 310 console = { 311 entry_type = core.MENU_ENTRY, 312 name = function() 313 return color.highlight("C") .. "ons: " .. core.getConsoleName() 314 end, 315 func = function() 316 core.nextConsoleChoice() 317 end, 318 alias = {"c", "C"}, 319 }, 320 prompt = { 321 entry_type = core.MENU_RETURN, 322 name = color.highlight("Esc") .. "ape to loader prompt", 323 func = function() 324 loader.setenv("autoboot_delay", "NO") 325 end, 326 alias = {core.KEYSTR_ESCAPE}, 327 }, 328 reboot = { 329 entry_type = core.MENU_ENTRY, 330 name = color.highlight("R") .. "eboot", 331 func = function() 332 loader.perform("reboot") 333 end, 334 alias = {"r", "R"}, 335 }, 336 kernel_options = { 337 entry_type = core.MENU_CAROUSEL_ENTRY, 338 carousel_id = "kernel", 339 items = core.kernelList, 340 name = function(idx, choice, all_choices) 341 if #all_choices == 0 then 342 return "" 343 end 344 345 local kernel_name 346 local name_color 347 if idx == 1 then 348 name_color = color.escapefg(color.GREEN) 349 else 350 name_color = color.escapefg(color.CYAN) 351 end 352 kernel_name = name_color .. choice .. 353 color.resetfg() 354 return kernel_name .. " (" .. idx .. " of " .. 355 #all_choices .. ")" 356 end, 357 func = function(_, choice, _) 358 if loader.getenv("kernelname") ~= nil then 359 loader.perform("unload") 360 end 361 config.selectKernel(choice) 362 end, 363 alias = {"k", "K"}, 364 }, 365 boot_options = { 366 entry_type = core.MENU_SUBMENU, 367 name = "Boot " .. color.highlight("O") .. "ptions", 368 submenu = menu.boot_options, 369 alias = {"o", "O"}, 370 }, 371 zpool_checkpoints = { 372 entry_type = core.MENU_ENTRY, 373 name = function() 374 local rewind = "No" 375 if core.isRewinded() then 376 rewind = "Yes" 377 end 378 return "Rewind ZFS " .. color.highlight("C") .. 379 "heckpoint: " .. rewind 380 end, 381 func = function() 382 core.changeRewindCheckpoint() 383 if core.isRewinded() then 384 bootenvSet( 385 core.bootenvDefaultRewinded()) 386 else 387 bootenvSet(core.bootenvDefault()) 388 end 389 config.setCarouselIndex("be_active", 1) 390 end, 391 visible = function() 392 return core.isZFSBoot() and 393 core.isCheckpointed() 394 end, 395 alias = {"c", "C"}, 396 }, 397 boot_envs = { 398 entry_type = core.MENU_SUBMENU, 399 visible = function() 400 return core.isZFSBoot() and 401 #core.bootenvList() > 1 402 end, 403 name = "Boot " .. color.highlight("E") .. "nvironments", 404 submenu = menu.boot_environments, 405 alias = {"e", "E"}, 406 }, 407 chainload = { 408 entry_type = core.MENU_ENTRY, 409 name = function() 410 return 'Chain' .. color.highlight("L") .. 411 "oad " .. loader.getenv('chain_disk') 412 end, 413 func = function() 414 loader.perform("chain " .. 415 loader.getenv('chain_disk')) 416 end, 417 visible = function() 418 return loader.getenv('chain_disk') ~= nil 419 end, 420 alias = {"l", "L"}, 421 }, 422 loader_needs_upgrade = { 423 entry_type = core.MENU_SEPARATOR, 424 name = function() 425 return color.highlight("Loader needs to be updated") 426 end, 427 visible = function() 428 return core.loaderTooOld() 429 end 430 }, 431 vendor = { 432 entry_type = core.MENU_ENTRY, 433 visible = function() 434 return false 435 end 436 }, 437 }, 438} 439 440menu.default = menu.welcome 441-- current_alias_table will be used to keep our alias table consistent across 442-- screen redraws, instead of relying on whatever triggered the redraw to update 443-- the local alias_table in menu.process. 444menu.current_alias_table = {} 445 446function menu.draw(menudef) 447 -- Clear the screen, reset the cursor, then draw 448 screen.clear() 449 menu.current_alias_table = drawer.drawscreen(menudef) 450 drawn_menu = menudef 451 screen.defcursor() 452end 453 454-- 'keypress' allows the caller to indicate that a key has been pressed that we 455-- should process as our initial input. 456function menu.process(menudef, keypress) 457 assert(menudef ~= nil) 458 459 if drawn_menu ~= menudef then 460 menu.draw(menudef) 461 end 462 463 while true do 464 local key = keypress or io.getchar() 465 keypress = nil 466 467 -- Special key behaviors 468 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 469 menudef ~= menu.default then 470 break 471 elseif key == core.KEY_ENTER then 472 core.boot() 473 -- Should not return. If it does, escape menu handling 474 -- and drop to loader prompt. 475 return false 476 end 477 478 key = string.char(key) 479 -- check to see if key is an alias 480 local sel_entry = nil 481 for k, v in pairs(menu.current_alias_table) do 482 if key == k then 483 sel_entry = v 484 break 485 end 486 end 487 488 -- if we have an alias do the assigned action: 489 if sel_entry ~= nil then 490 local handler = menu.handlers[sel_entry.entry_type] 491 assert(handler ~= nil) 492 -- The handler's return value indicates if we 493 -- need to exit this menu. An omitted or true 494 -- return value means to continue. 495 if handler(menudef, sel_entry) == false then 496 return 497 end 498 -- If we got an alias key the screen is out of date... 499 -- redraw it. 500 menu.draw(menudef) 501 end 502 end 503end 504 505function menu.run() 506 local autoboot_key 507 local delay = loader.getenv("autoboot_delay") 508 509 if delay ~= nil and delay:lower() == "no" then 510 delay = nil 511 else 512 delay = tonumber(delay) or 10 513 end 514 515 if delay == -1 then 516 core.boot() 517 return 518 end 519 520 menu.draw(menu.default) 521 522 if delay ~= nil then 523 autoboot_key = menu.autoboot(delay) 524 525 -- autoboot_key should return the key pressed. It will only 526 -- return nil if we hit the timeout and executed the timeout 527 -- command. Bail out. 528 if autoboot_key == nil then 529 return 530 end 531 end 532 533 menu.process(menu.default, autoboot_key) 534 drawn_menu = nil 535 536 screen.defcursor() 537 -- We explicitly want the newline print adds 538 print("Exiting menu!") 539end 540 541function menu.autoboot(delay) 542 local x = loader.getenv("loader_menu_timeout_x") or 4 543 local y = loader.getenv("loader_menu_timeout_y") or 24 544 local endtime = loader.time() + delay 545 local time 546 local last 547 repeat 548 time = endtime - loader.time() 549 if last == nil or last ~= time then 550 last = time 551 screen.setcursor(x, y) 552 printc("Autoboot in " .. time .. 553 " seconds. [Space] to pause ") 554 screen.defcursor() 555 end 556 if io.ischar() then 557 local ch = io.getchar() 558 if ch == core.KEY_ENTER then 559 break 560 else 561 -- Erase autoboot msg. While real VT100s 562 -- wouldn't scroll when receiving a char with 563 -- the cursor at (79, 24), bad emulators do. 564 -- Avoid the issue by stopping at 79. 565 screen.setcursor(1, y) 566 printc(string.rep(" ", 79)) 567 screen.defcursor() 568 return ch 569 end 570 end 571 572 loader.delay(50000) 573 until time <= 0 574 575 local cmd = loader.getenv("menu_timeout_command") or "boot" 576 cli_execute_unparsed(cmd) 577 return nil 578end 579 580-- CLI commands 581function cli.menu() 582 menu.run() 583end 584 585return menu 586