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