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