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