1-- 2-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3-- 4-- Copyright (c) 2015 Pedro Souza <[email protected]> 5-- Copyright (c) 2018 Kyle Evans <[email protected]> 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 = false, 407 }, 408 }, 409} 410 411menu.default = menu.welcome 412-- current_alias_table will be used to keep our alias table consistent across 413-- screen redraws, instead of relying on whatever triggered the redraw to update 414-- the local alias_table in menu.process. 415menu.current_alias_table = {} 416 417function menu.draw(menudef) 418 -- Clear the screen, reset the cursor, then draw 419 screen.clear() 420 menu.current_alias_table = drawer.drawscreen(menudef) 421 drawn_menu = menudef 422 screen.defcursor() 423end 424 425-- 'keypress' allows the caller to indicate that a key has been pressed that we 426-- should process as our initial input. 427function menu.process(menudef, keypress) 428 assert(menudef ~= nil) 429 430 if drawn_menu ~= menudef then 431 menu.draw(menudef) 432 end 433 434 while true do 435 local key = keypress or io.getchar() 436 keypress = nil 437 438 -- Special key behaviors 439 if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and 440 menudef ~= menu.default then 441 break 442 elseif key == core.KEY_ENTER then 443 core.boot() 444 -- Should not return. If it does, escape menu handling 445 -- and drop to loader prompt. 446 return false 447 end 448 449 key = string.char(key) 450 -- check to see if key is an alias 451 local sel_entry = nil 452 for k, v in pairs(menu.current_alias_table) do 453 if key == k then 454 sel_entry = v 455 break 456 end 457 end 458 459 -- if we have an alias do the assigned action: 460 if sel_entry ~= nil then 461 local handler = menu.handlers[sel_entry.entry_type] 462 assert(handler ~= nil) 463 -- The handler's return value indicates if we 464 -- need to exit this menu. An omitted or true 465 -- return value means to continue. 466 if handler(menudef, sel_entry) == false then 467 return 468 end 469 -- If we got an alias key the screen is out of date... 470 -- redraw it. 471 menu.draw(menudef) 472 end 473 end 474end 475 476function menu.run() 477 local autoboot_key 478 local delay = loader.getenv("autoboot_delay") 479 480 if delay ~= nil and delay:lower() == "no" then 481 delay = nil 482 else 483 delay = tonumber(delay) or 10 484 end 485 486 if delay == -1 then 487 core.boot() 488 return 489 end 490 491 menu.draw(menu.default) 492 493 if delay ~= nil then 494 autoboot_key = menu.autoboot(delay) 495 496 -- autoboot_key should return the key pressed. It will only 497 -- return nil if we hit the timeout and executed the timeout 498 -- command. Bail out. 499 if autoboot_key == nil then 500 return 501 end 502 end 503 504 menu.process(menu.default, autoboot_key) 505 drawn_menu = nil 506 507 screen.defcursor() 508 print("Exiting menu!") 509end 510 511function menu.autoboot(delay) 512 local x = loader.getenv("loader_menu_timeout_x") or 4 513 local y = loader.getenv("loader_menu_timeout_y") or 23 514 local endtime = loader.time() + delay 515 local time 516 local last 517 repeat 518 time = endtime - loader.time() 519 if last == nil or last ~= time then 520 last = time 521 screen.setcursor(x, y) 522 print("Autoboot in " .. time .. 523 " seconds, hit [Enter] to boot" .. 524 " or any other key to stop ") 525 screen.defcursor() 526 end 527 if io.ischar() then 528 local ch = io.getchar() 529 if ch == core.KEY_ENTER then 530 break 531 else 532 -- erase autoboot msg 533 screen.setcursor(0, y) 534 print(string.rep(" ", 80)) 535 screen.defcursor() 536 return ch 537 end 538 end 539 540 loader.delay(50000) 541 until time <= 0 542 543 local cmd = loader.getenv("menu_timeout_command") or "boot" 544 cli_execute_unparsed(cmd) 545 return nil 546end 547 548-- CLI commands 549function cli.menu() 550 menu.run() 551end 552 553return menu 554