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