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