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