1-- 2-- SPDX-License-Identifier: BSD-2-Clause 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 30local config = require("config") 31local hook = require("hook") 32 33local core = {} 34 35local default_acpi = false 36local default_safe_mode = false 37local default_single_user = false 38local default_verbose = false 39 40local bootenv_list = "bootenvs" 41 42local function composeLoaderCmd(cmd_name, argstr) 43 if argstr ~= nil then 44 cmd_name = cmd_name .. " " .. argstr 45 end 46 return cmd_name 47end 48 49local function recordDefaults() 50 local boot_single = loader.getenv("boot_single") or "no" 51 local boot_verbose = loader.getenv("boot_verbose") or "no" 52 53 default_acpi = core.getACPI() 54 default_single_user = boot_single:lower() ~= "no" 55 default_verbose = boot_verbose:lower() ~= "no" 56 57 core.setACPI(default_acpi) 58 core.setSingleUser(default_single_user) 59 core.setVerbose(default_verbose) 60end 61 62 63-- Globals 64-- try_include will return the loaded module on success, or false and the error 65-- message on failure. 66function try_include(module) 67 if module:sub(1, 1) ~= "/" then 68 module = loader.lua_path .. "/" .. module 69 -- We only attempt to append an extension if an absolute path 70 -- wasn't specified. This assumes that the caller either wants 71 -- to treat this like it would require() and specify just the 72 -- base filename, or they know what they're doing as they've 73 -- specified an absolute path and we shouldn't impede. 74 if module:match(".lua$") == nil then 75 module = module .. ".lua" 76 end 77 end 78 if lfs.attributes(module, "mode") ~= "file" then 79 return 80 end 81 82 return dofile(module) 83end 84 85-- Module exports 86-- Commonly appearing constants 87core.KEY_BACKSPACE = 8 88core.KEY_ENTER = 13 89core.KEY_DELETE = 127 90 91-- Note that this is a decimal representation, despite the leading 0 that in 92-- other contexts (outside of Lua) may mean 'octal' 93core.KEYSTR_ESCAPE = "\027" 94core.KEYSTR_CSI = core.KEYSTR_ESCAPE .. "[" 95core.KEYSTR_RESET = core.KEYSTR_ESCAPE .. "c" 96 97core.MENU_RETURN = "return" 98core.MENU_ENTRY = "entry" 99core.MENU_SEPARATOR = "separator" 100core.MENU_SUBMENU = "submenu" 101core.MENU_CAROUSEL_ENTRY = "carousel_entry" 102 103function core.setVerbose(verbose) 104 if verbose == nil then 105 verbose = not core.verbose 106 end 107 108 if verbose then 109 loader.setenv("boot_verbose", "YES") 110 else 111 loader.unsetenv("boot_verbose") 112 end 113 core.verbose = verbose 114end 115 116function core.setSingleUser(single_user) 117 if single_user == nil then 118 single_user = not core.su 119 end 120 121 if single_user then 122 loader.setenv("boot_single", "YES") 123 else 124 loader.unsetenv("boot_single") 125 end 126 core.su = single_user 127end 128 129function core.hasACPI() 130 -- We can't trust acpi.rsdp to be set if the loader binary doesn't do 131 -- ACPI detection early enough. UEFI loader historically didn't, so 132 -- we'll fallback to assuming ACPI is enabled if this binary does not 133 -- declare that it probes for ACPI early enough 134 if loader.getenv("acpi.rsdp") ~= nil then 135 return true 136 end 137 138 return not core.hasFeature("EARLY_ACPI") 139end 140 141function core.getACPI() 142 if not core.hasACPI() then 143 return false 144 end 145 146 -- Otherwise, respect disabled if it's set 147 local c = loader.getenv("hint.acpi.0.disabled") 148 return c == nil or tonumber(c) ~= 1 149end 150 151function core.setACPI(acpi) 152 if acpi == nil then 153 acpi = not core.acpi 154 end 155 156 if acpi then 157 config.enableModule("acpi") 158 loader.setenv("hint.acpi.0.disabled", "0") 159 else 160 config.disableModule("acpi") 161 loader.setenv("hint.acpi.0.disabled", "1") 162 end 163 core.acpi = acpi 164end 165 166function core.setSafeMode(safe_mode) 167 if safe_mode == nil then 168 safe_mode = not core.sm 169 end 170 if safe_mode then 171 loader.setenv("kern.smp.disabled", "1") 172 loader.setenv("hw.ata.ata_dma", "0") 173 loader.setenv("hw.ata.atapi_dma", "0") 174 loader.setenv("kern.eventtimer.periodic", "1") 175 loader.setenv("kern.geom.part.check_integrity", "0") 176 else 177 loader.unsetenv("kern.smp.disabled") 178 loader.unsetenv("hw.ata.ata_dma") 179 loader.unsetenv("hw.ata.atapi_dma") 180 loader.unsetenv("kern.eventtimer.periodic") 181 loader.unsetenv("kern.geom.part.check_integrity") 182 end 183 core.sm = safe_mode 184end 185 186function core.clearCachedKernels() 187 -- Clear the kernel cache on config changes, autodetect might have 188 -- changed or if we've switched boot environments then we could have 189 -- a new kernel set. 190 core.cached_kernels = nil 191end 192 193function core.kernelList() 194 if core.cached_kernels ~= nil then 195 return core.cached_kernels 196 end 197 198 local default_kernel = loader.getenv("kernel") 199 local v = loader.getenv("kernels") 200 local autodetect = loader.getenv("kernels_autodetect") or "" 201 202 local kernels = {} 203 local unique = {} 204 local i = 0 205 206 if default_kernel then 207 i = i + 1 208 kernels[i] = default_kernel 209 unique[default_kernel] = true 210 end 211 212 if v ~= nil then 213 for n in v:gmatch("([^;, ]+)[;, ]?") do 214 if unique[n] == nil then 215 i = i + 1 216 kernels[i] = n 217 unique[n] = true 218 end 219 end 220 end 221 222 -- Do not attempt to autodetect if underlying filesystem 223 -- do not support directory listing (e.g. tftp, http) 224 if not lfs.attributes("/boot", "mode") then 225 autodetect = "no" 226 loader.setenv("kernels_autodetect", "NO") 227 end 228 229 -- Base whether we autodetect kernels or not on a loader.conf(5) 230 -- setting, kernels_autodetect. If it's set to 'yes', we'll add 231 -- any kernels we detect based on the criteria described. 232 if autodetect:lower() ~= "yes" then 233 core.cached_kernels = kernels 234 return core.cached_kernels 235 end 236 237 local present = {} 238 239 -- Automatically detect other bootable kernel directories using a 240 -- heuristic. Any directory in /boot that contains an ordinary file 241 -- named "kernel" is considered eligible. 242 for file, ftype in lfs.dir("/boot") do 243 local fname = "/boot/" .. file 244 245 if file == "." or file == ".." then 246 goto continue 247 end 248 249 if ftype then 250 if ftype ~= lfs.DT_DIR then 251 goto continue 252 end 253 elseif lfs.attributes(fname, "mode") ~= "directory" then 254 goto continue 255 end 256 257 if lfs.attributes(fname .. "/kernel", "mode") ~= "file" then 258 goto continue 259 end 260 261 if unique[file] == nil then 262 i = i + 1 263 kernels[i] = file 264 unique[file] = true 265 end 266 267 present[file] = true 268 269 ::continue:: 270 end 271 272 -- If we found more than one kernel, prune the "kernel" specified kernel 273 -- off of the list if it wasn't found during traversal. If we didn't 274 -- actually find any kernels, we just assume that they know what they're 275 -- doing and leave it alone. 276 if default_kernel and not present[default_kernel] and #kernels > 1 then 277 for i = 1, #kernels do 278 if i == #kernels then 279 kernels[i] = nil 280 else 281 kernels[i] = kernels[i + 1] 282 end 283 end 284 end 285 286 core.cached_kernels = kernels 287 return core.cached_kernels 288end 289 290function core.bootenvDefault() 291 return loader.getenv("zfs_be_active") 292end 293 294function core.bootenvList() 295 local bootenv_count = tonumber(loader.getenv(bootenv_list .. "_count")) 296 local bootenvs = {} 297 local curenv 298 local envcount = 0 299 local unique = {} 300 301 if bootenv_count == nil or bootenv_count <= 0 then 302 return bootenvs 303 end 304 305 -- Currently selected bootenv is always first/default 306 -- On the rewinded list the bootenv may not exists 307 if core.isRewinded() then 308 curenv = core.bootenvDefaultRewinded() 309 else 310 curenv = core.bootenvDefault() 311 end 312 if curenv ~= nil then 313 envcount = envcount + 1 314 bootenvs[envcount] = curenv 315 unique[curenv] = true 316 end 317 318 for curenv_idx = 0, bootenv_count - 1 do 319 curenv = loader.getenv(bootenv_list .. "[" .. curenv_idx .. "]") 320 if curenv ~= nil and unique[curenv] == nil then 321 envcount = envcount + 1 322 bootenvs[envcount] = curenv 323 unique[curenv] = true 324 end 325 end 326 return bootenvs 327end 328 329function core.isCheckpointed() 330 return loader.getenv("zpool_checkpoint") ~= nil 331end 332 333function core.bootenvDefaultRewinded() 334 local defname = "zfs:!" .. string.sub(core.bootenvDefault(), 5) 335 local bootenv_count = tonumber("bootenvs_check_count") 336 337 if bootenv_count == nil or bootenv_count <= 0 then 338 return defname 339 end 340 341 for curenv_idx = 0, bootenv_count - 1 do 342 local curenv = loader.getenv("bootenvs_check[" .. curenv_idx .. "]") 343 if curenv == defname then 344 return defname 345 end 346 end 347 348 return loader.getenv("bootenvs_check[0]") 349end 350 351function core.isRewinded() 352 return bootenv_list == "bootenvs_check" 353end 354 355function core.changeRewindCheckpoint() 356 if core.isRewinded() then 357 bootenv_list = "bootenvs" 358 else 359 bootenv_list = "bootenvs_check" 360 end 361end 362 363function core.loadEntropy() 364 if core.isUEFIBoot() then 365 if (loader.getenv("entropy_efi_seed") or "no"):lower() == "yes" then 366 loader.perform("efi-seed-entropy") 367 end 368 end 369end 370 371function core.setDefaults() 372 core.setACPI(default_acpi) 373 core.setSafeMode(default_safe_mode) 374 core.setSingleUser(default_single_user) 375 core.setVerbose(default_verbose) 376end 377 378function core.autoboot(argstr) 379 -- loadelf() only if we've not already loaded a kernel 380 if loader.getenv("kernelname") == nil then 381 config.loadelf() 382 end 383 core.loadEntropy() 384 loader.perform(composeLoaderCmd("autoboot", argstr)) 385end 386 387function core.boot(argstr) 388 -- loadelf() only if we've not already loaded a kernel 389 if loader.getenv("kernelname") == nil then 390 config.loadelf() 391 end 392 core.loadEntropy() 393 loader.perform(composeLoaderCmd("boot", argstr)) 394end 395 396function core.hasFeature(name) 397 if not loader.has_feature then 398 -- Loader too old, no feature support 399 return nil, "No feature support in loaded loader" 400 end 401 402 return loader.has_feature(name) 403end 404 405function core.isSingleUserBoot() 406 local single_user = loader.getenv("boot_single") 407 return single_user ~= nil and single_user:lower() == "yes" 408end 409 410function core.isUEFIBoot() 411 local efiver = loader.getenv("efi-version") 412 413 return efiver ~= nil 414end 415 416function core.isZFSBoot() 417 local c = loader.getenv("currdev") 418 419 if c ~= nil then 420 return c:match("^zfs:") ~= nil 421 end 422 return false 423end 424 425function core.isFramebufferConsole() 426 local c = loader.getenv("console") 427 if c ~= nil then 428 if c:find("efi") == nil and c:find("vidconsole") == nil then 429 return false 430 end 431 if loader.getenv("screen.depth") ~= nil then 432 return true 433 end 434 end 435 return false 436end 437 438function core.isSerialConsole() 439 local c = loader.getenv("console") 440 if c ~= nil then 441 -- serial console is comconsole, but also userboot. 442 -- userboot is there, because we have no way to know 443 -- if the user terminal can draw unicode box chars or not. 444 if c:find("comconsole") ~= nil or c:find("userboot") ~= nil then 445 return true 446 end 447 end 448 return false 449end 450 451function core.isSerialBoot() 452 local s = loader.getenv("boot_serial") 453 if s ~= nil then 454 return true 455 end 456 457 local m = loader.getenv("boot_multicons") 458 if m ~= nil then 459 return true 460 end 461 return false 462end 463 464-- Is the menu skipped in the environment in which we've booted? 465function core.isMenuSkipped() 466 return string.lower(loader.getenv("beastie_disable") or "") == "yes" 467end 468 469-- This may be a better candidate for a 'utility' module. 470function core.deepCopyTable(tbl) 471 local new_tbl = {} 472 for k, v in pairs(tbl) do 473 if type(v) == "table" then 474 new_tbl[k] = core.deepCopyTable(v) 475 else 476 new_tbl[k] = v 477 end 478 end 479 return new_tbl 480end 481 482-- XXX This should go away if we get the table lib into shape for importing. 483-- As of now, it requires some 'os' functions, so we'll implement this in lua 484-- for our uses 485function core.popFrontTable(tbl) 486 -- Shouldn't reasonably happen 487 if #tbl == 0 then 488 return nil, nil 489 elseif #tbl == 1 then 490 return tbl[1], {} 491 end 492 493 local first_value = tbl[1] 494 local new_tbl = {} 495 -- This is not a cheap operation 496 for k, v in ipairs(tbl) do 497 if k > 1 then 498 new_tbl[k - 1] = v 499 end 500 end 501 502 return first_value, new_tbl 503end 504 505function core.getConsoleName() 506 if loader.getenv("boot_multicons") ~= nil then 507 if loader.getenv("boot_serial") ~= nil then 508 return "Dual (Serial primary)" 509 else 510 return "Dual (Video primary)" 511 end 512 else 513 if loader.getenv("boot_serial") ~= nil then 514 return "Serial" 515 else 516 return "Video" 517 end 518 end 519end 520 521function core.nextConsoleChoice() 522 if loader.getenv("boot_multicons") ~= nil then 523 if loader.getenv("boot_serial") ~= nil then 524 loader.unsetenv("boot_serial") 525 else 526 loader.unsetenv("boot_multicons") 527 loader.setenv("boot_serial", "YES") 528 end 529 else 530 if loader.getenv("boot_serial") ~= nil then 531 loader.unsetenv("boot_serial") 532 else 533 loader.setenv("boot_multicons", "YES") 534 loader.setenv("boot_serial", "YES") 535 end 536 end 537end 538 539recordDefaults() 540hook.register("config.reloaded", core.clearCachedKernels) 541return core 542