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