Index: head/stand/lua/core.lua =================================================================== --- head/stand/lua/core.lua (revision 329730) +++ head/stand/lua/core.lua (revision 329731) @@ -1,335 +1,379 @@ -- -- Copyright (c) 2015 Pedro Souza -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions -- are met: -- 1. Redistributions of source code must retain the above copyright -- notice, this list of conditions and the following disclaimer. -- 2. Redistributions in binary form must reproduce the above copyright -- notice, this list of conditions and the following disclaimer in the -- documentation and/or other materials provided with the distribution. -- -- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -- SUCH DAMAGE. -- -- $FreeBSD$ -- local config = require('config') local core = {} local compose_loader_cmd = function(cmd_name, argstr) if argstr ~= nil then cmd_name = cmd_name .. " " .. argstr end return cmd_name end -- Internal function -- Parses arguments to boot and returns two values: kernel_name, argstr -- Defaults to nil and "" respectively. -- This will also parse arguments to autoboot, but the with_kernel argument -- will need to be explicitly overwritten to false local parse_boot_args = function(argv, with_kernel) if with_kernel == nil then with_kernel = true end if #argv == 0 then if with_kernel then return nil, "" else return "" end end local kernel_name local argstr = "" for k, v in ipairs(argv) do if with_kernel and v:sub(1,1) ~= "-" then kernel_name = v else argstr = argstr .. " " .. v end end if with_kernel then return kernel_name, argstr else return argstr end end -- Globals function boot(...) local argv = {...} local cmd_name = "" cmd_name, argv = core.popFrontTable(argv) local kernel, argstr = parse_boot_args(argv) if kernel ~= nil then loader.perform("unload") config.selectkernel(kernel) end core.boot(argstr) end function autoboot(...) local argv = {...} local cmd_name = "" cmd_name, argv = core.popFrontTable(argv) local argstr = parse_boot_args(argv, false) core.autoboot(argstr) end -- Module exports -- Commonly appearing constants core.KEY_BACKSPACE = 8 core.KEY_ENTER = 13 core.KEY_DELETE = 127 core.KEYSTR_ESCAPE = "\027" core.MENU_RETURN = "return" core.MENU_ENTRY = "entry" core.MENU_SEPARATOR = "separator" core.MENU_SUBMENU = "submenu" core.MENU_CAROUSEL_ENTRY = "carousel_entry" function core.setVerbose(b) if b == nil then b = not core.verbose end if b then loader.setenv("boot_verbose", "YES") else loader.unsetenv("boot_verbose") end core.verbose = b end function core.setSingleUser(b) if b == nil then b = not core.su end if b then loader.setenv("boot_single", "YES") else loader.unsetenv("boot_single") end core.su = b end function core.getACPIPresent(checkingSystemDefaults) local c = loader.getenv("hint.acpi.0.rsdp") if c ~= nil then if checkingSystemDefaults then return true end -- Otherwise, respect disabled if it's set c = loader.getenv("hint.acpi.0.disabled") return c == nil or tonumber(c) ~= 1 end return false end function core.setACPI(b) if b == nil then b = not core.acpi end if b then loader.setenv("acpi_load", "YES") loader.setenv("hint.acpi.0.disabled", "0") loader.unsetenv("loader.acpi_disabled_by_user") else loader.unsetenv("acpi_load") loader.setenv("hint.acpi.0.disabled", "1") loader.setenv("loader.acpi_disabled_by_user", "1") end core.acpi = b end function core.setSafeMode(b) if b == nil then b = not core.sm end if b then loader.setenv("kern.smp.disabled", "1") loader.setenv("hw.ata.ata_dma", "0") loader.setenv("hw.ata.atapi_dma", "0") loader.setenv("hw.ata.wc", "0") loader.setenv("hw.eisa_slots", "0") loader.setenv("kern.eventtimer.periodic", "1") loader.setenv("kern.geom.part.check_integrity", "0") else loader.unsetenv("kern.smp.disabled") loader.unsetenv("hw.ata.ata_dma") loader.unsetenv("hw.ata.atapi_dma") loader.unsetenv("hw.ata.wc") loader.unsetenv("hw.eisa_slots") loader.unsetenv("kern.eventtimer.periodic") loader.unsetenv("kern.geom.part.check_integrity") end core.sm = b end function core.kernelList() local k = loader.getenv("kernel") local v = loader.getenv("kernels") local kernels = {} local unique = {} local i = 0 if k ~= nil then i = i + 1 kernels[i] = k unique[k] = true end if v ~= nil then for n in v:gmatch("([^; ]+)[; ]?") do if unique[n] == nil then i = i + 1 kernels[i] = n unique[n] = true end end -- We will not automatically detect kernels to be displayed if -- loader.conf(5) explicitly set 'kernels'. return kernels end -- Automatically detect other bootable kernel directories using a -- heuristic. Any directory in /boot that contains an ordinary file -- named "kernel" is considered eligible. for file in lfs.dir("/boot") do local fname = "/boot/" .. file if file == "." or file == ".." then goto continue end if lfs.attributes(fname, "mode") ~= "directory" then goto continue end if lfs.attributes(fname .. "/kernel", "mode") ~= "file" then goto continue end if unique[file] == nil then i = i + 1 kernels[i] = file unique[file] = true end ::continue:: end return kernels end +function core.bootenvDefault() + return loader.getenv("zfs_be_active") +end + +function core.bootenvList() + local bootenv_count = tonumber(loader.getenv("bootenvs_count")) + local bootenvs = {} + local curenv + local curenv_idx = 0 + local envcount = 0 + local unique = {} + + if bootenv_count == nil or bootenv_count <= 0 then + return bootenvs + end + + -- Currently selected bootenv is always first/default + curenv = core.bootenvDefault() + if curenv ~= nil then + envcount = envcount + 1 + bootenvs[envcount] = curenv + unique[curenv] = true + end + + for curenv_idx = 0, bootenv_count - 1 do + curenv = loader.getenv("bootenvs[" .. curenv_idx .. "]") + if curenv ~= nil and unique[curenv] == nil then + envcount = envcount + 1 + bootenvs[envcount] = curenv + unique[curenv] = true + end + end + return bootenvs +end + function core.setDefaults() core.setACPI(core.getACPIPresent(true)) core.setSafeMode(false) core.setSingleUser(false) core.setVerbose(false) end function core.autoboot(argstr) config.loadelf() loader.perform(compose_loader_cmd("autoboot", argstr)) end function core.boot(argstr) config.loadelf() loader.perform(compose_loader_cmd("boot", argstr)) end function core.isSingleUserBoot() local single_user = loader.getenv("boot_single") return single_user ~= nil and single_user:lower() == "yes" +end + +function core.isZFSBoot() + local c = loader.getenv("currdev") + + if c ~= nil then + return c:match("^zfs:") ~= nil + end + return false end function core.isSerialBoot() local c = loader.getenv("console") if c ~= nil then if c:find("comconsole") ~= nil then return true end end local s = loader.getenv("boot_serial") if s ~= nil then return true end local m = loader.getenv("boot_multicons") if m ~= nil then return true end return false end function core.isSystem386() return loader.machine_arch == "i386" end -- This may be a better candidate for a 'utility' module. function core.shallowCopyTable(tbl) local new_tbl = {} for k, v in pairs(tbl) do if type(v) == "table" then new_tbl[k] = core.shallowCopyTable(v) else new_tbl[k] = v end end return new_tbl end -- XXX This should go away if we get the table lib into shape for importing. -- As of now, it requires some 'os' functions, so we'll implement this in lua -- for our uses function core.popFrontTable(tbl) -- Shouldn't reasonably happen if #tbl == 0 then return nil, nil elseif #tbl == 1 then return tbl[1], {} end local first_value = tbl[1] local new_tbl = {} -- This is not a cheap operation for k, v in ipairs(tbl) do if k > 1 then new_tbl[k - 1] = v end end return first_value, new_tbl end -- On i386, hint.acpi.0.rsdp will be set before we're loaded. On !i386, it will -- generally be set upon execution of the kernel. Because of this, we can't (or -- don't really want to) detect/disable ACPI on !i386 reliably. Just set it -- enabled if we detect it and leave well enough alone if we don't. if core.isSystem386() and core.getACPIPresent(false) then core.setACPI(true) end return core Index: head/stand/lua/menu.lua =================================================================== --- head/stand/lua/menu.lua (revision 329730) +++ head/stand/lua/menu.lua (revision 329731) @@ -1,410 +1,479 @@ -- -- Copyright (c) 2015 Pedro Souza -- Copyright (C) 2018 Kyle Evans -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions -- are met: -- 1. Redistributions of source code must retain the above copyright -- notice, this list of conditions and the following disclaimer. -- 2. Redistributions in binary form must reproduce the above copyright -- notice, this list of conditions and the following disclaimer in the -- documentation and/or other materials provided with the distribution. -- -- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND -- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE -- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -- SUCH DAMAGE. -- -- $FreeBSD$ -- local core = require("core") local color = require("color") local config = require("config") local screen = require("screen") local drawer = require("drawer") local menu = {} local skip local run local autoboot local OnOff = function(str, b) if b then return str .. color.escapef(color.GREEN) .. "On" .. color.escapef(color.WHITE) else return str .. color.escapef(color.RED) .. "off" .. color.escapef(color.WHITE) end end +local bootenvSet = function(env) + loader.setenv("vfs.root.mountfrom", env) + loader.setenv("currdev", env .. ":") + config.reload() +end + -- Module exports menu.handlers = { -- Menu handlers take the current menu and selected entry as parameters, -- and should return a boolean indicating whether execution should -- continue or not. The return value may be omitted if this entry should -- have no bearing on whether we continue or not, indicating that we -- should just continue after execution. [core.MENU_ENTRY] = function(current_menu, entry) -- run function entry.func() end, [core.MENU_CAROUSEL_ENTRY] = function(current_menu, entry) -- carousel (rotating) functionality local carid = entry.carousel_id local caridx = config.getCarouselIndex(carid) local choices = entry.items if type(choices) == "function" then choices = choices() end if #choices > 0 then caridx = (caridx % #choices) + 1 config.setCarouselIndex(carid, caridx) entry.func(caridx, choices[caridx], choices) end end, [core.MENU_SUBMENU] = function(current_menu, entry) -- recurse return menu.run(entry.submenu) end, [core.MENU_RETURN] = function(current_menu, entry) -- allow entry to have a function/side effect if entry.func ~= nil then entry.func() end return false end, } -- loader menu tree is rooted at menu.welcome +menu.boot_environments = { + entries = { + -- return to welcome menu + { + entry_type = core.MENU_RETURN, + name = "Back to main menu" .. + color.highlight(" [Backspace]"), + }, + { + entry_type = core.MENU_CAROUSEL_ENTRY, + carousel_id = "be_active", + items = core.bootenvList, + name = function(idx, choice, all_choices) + if #all_choices == 0 then + return "Active: " + end + + local is_default = (idx == 1) + local bootenv_name = "" + local name_color + if is_default then + name_color = color.escapef(color.GREEN) + else + name_color = color.escapef(color.BLUE) + end + bootenv_name = bootenv_name .. name_color .. + choice .. color.default() + return color.highlight("A").."ctive: " .. + bootenv_name .. " (" .. idx .. " of " .. + #all_choices .. ")" + end, + func = function(idx, choice, all_choices) + bootenvSet(choice) + end, + alias = {"a", "A"}, + }, + { + entry_type = core.MENU_ENTRY, + name = function() + return color.highlight("b") .. "ootfs: " .. + core.bootenvDefault() + end, + func = function() + -- Reset active boot environment to the default + config.setCarouselIndex("be_active", 1) + bootenvSet(core.bootenvDefault()) + end, + alias = {"b", "B"}, + }, + }, +} + menu.boot_options = { entries = { -- return to welcome menu { entry_type = core.MENU_RETURN, name = "Back to main menu" .. color.highlight(" [Backspace]"), }, -- load defaults { entry_type = core.MENU_ENTRY, name = "Load System " .. color.highlight("D") .. "efaults", func = core.setDefaults, alias = {"d", "D"} }, { entry_type = core.MENU_SEPARATOR, }, { entry_type = core.MENU_SEPARATOR, name = "Boot Options:", }, -- acpi { entry_type = core.MENU_ENTRY, visible = core.isSystem386, name = function() return OnOff(color.highlight("A") .. "CPI :", core.acpi) end, func = core.setACPI, alias = {"a", "A"} }, -- safe mode { entry_type = core.MENU_ENTRY, name = function() return OnOff("Safe " .. color.highlight("M") .. "ode :", core.sm) end, func = core.setSafeMode, alias = {"m", "M"} }, -- single user { entry_type = core.MENU_ENTRY, name = function() return OnOff(color.highlight("S") .. "ingle user:", core.su) end, func = core.setSingleUser, alias = {"s", "S"} }, -- verbose boot { entry_type = core.MENU_ENTRY, name = function() return OnOff(color.highlight("V") .. "erbose :", core.verbose) end, func = core.setVerbose, alias = {"v", "V"} }, }, } menu.welcome = { entries = function() local menu_entries = menu.welcome.all_entries -- Swap the first two menu items on single user boot if core.isSingleUserBoot() then -- We'll cache the swapped menu, for performance if menu.welcome.swapped_menu ~= nil then return menu.welcome.swapped_menu end -- Shallow copy the table menu_entries = core.shallowCopyTable(menu_entries) -- Swap the first two menu entries menu_entries[1], menu_entries[2] = menu_entries[2], menu_entries[1] -- Then set their names to their alternate names menu_entries[1].name, menu_entries[2].name = menu_entries[1].alternate_name, menu_entries[2].alternate_name menu.welcome.swapped_menu = menu_entries end return menu_entries end, all_entries = { -- boot multi user { entry_type = core.MENU_ENTRY, name = color.highlight("B") .. "oot Multi user " .. color.highlight("[Enter]"), -- Not a standard menu entry function! alternate_name = color.highlight("B") .. "oot Multi user", func = function() core.setSingleUser(false) core.boot() end, alias = {"b", "B"} }, -- boot single user { entry_type = core.MENU_ENTRY, name = "Boot " .. color.highlight("S") .. "ingle user", -- Not a standard menu entry function! alternate_name = "Boot " .. color.highlight("S") .. "ingle user " .. color.highlight("[Enter]"), func = function() core.setSingleUser(true) core.boot() end, alias = {"s", "S"} }, -- escape to interpreter { entry_type = core.MENU_RETURN, name = color.highlight("Esc") .. "ape to loader prompt", func = function() loader.setenv("autoboot_delay", "NO") end, alias = {core.KEYSTR_ESCAPE} }, -- reboot { entry_type = core.MENU_ENTRY, name = color.highlight("R") .. "eboot", func = function() loader.perform("reboot") end, alias = {"r", "R"} }, { entry_type = core.MENU_SEPARATOR, }, { entry_type = core.MENU_SEPARATOR, name = "Options:", }, -- kernel options { entry_type = core.MENU_CAROUSEL_ENTRY, carousel_id = "kernel", items = core.kernelList, name = function(idx, choice, all_choices) if #all_choices == 0 then return "Kernel: " end local is_default = (idx == 1) local kernel_name = "" local name_color if is_default then name_color = color.escapef(color.GREEN) kernel_name = "default/" else name_color = color.escapef(color.BLUE) end kernel_name = kernel_name .. name_color .. choice .. color.default() return color.highlight("K") .. "ernel: " .. kernel_name .. " (" .. idx .. " of " .. #all_choices .. ")" end, func = function(idx, choice, all_choices) config.selectkernel(choice) end, alias = {"k", "K"} }, -- boot options { entry_type = core.MENU_SUBMENU, name = "Boot " .. color.highlight("O") .. "ptions", submenu = menu.boot_options, alias = {"o", "O"} + }, + -- boot environments + { + entry_type = core.MENU_SUBMENU, + visible = function() + return core.isZFSBoot() and + #core.bootenvList() > 1 + end, + name = "Boot " .. color.highlight("E") .. "nvironments", + submenu = menu.boot_environments, + alias = {"e", "E"}, }, }, } menu.default = menu.welcome function menu.run(m) if menu.skip() then core.autoboot() return false end if m == nil then m = menu.default end -- redraw screen screen.clear() screen.defcursor() local alias_table = drawer.drawscreen(m) -- Might return nil, that's ok local autoboot_key = menu.autoboot() cont = true while cont do local key = autoboot_key or io.getchar() autoboot_key = nil -- Special key behaviors if (key == core.KEY_BACKSPACE or key == core.KEY_DELETE) and m ~= menu.default then break elseif key == core.KEY_ENTER then core.boot() -- Should not return end key = string.char(key) -- check to see if key is an alias local sel_entry = nil for k, v in pairs(alias_table) do if key == k then sel_entry = v end end -- if we have an alias do the assigned action: if sel_entry ~= nil then -- Get menu handler local handler = menu.handlers[sel_entry.entry_type] if handler ~= nil then -- The handler's return value indicates whether -- we need to exit this menu. An omitted return -- value means "continue" by default. cont = handler(m, sel_entry) if cont == nil then cont = true end end -- if we got an alias key the screen is out of date: screen.clear() screen.defcursor() alias_table = drawer.drawscreen(m) end end if m == menu.default then screen.defcursor() print("Exiting menu!") return false end return true end function menu.skip() if core.isSerialBoot() then return true end local c = string.lower(loader.getenv("console") or "") if c:match("^efi[ ;]") ~= nil or c:match("[ ;]efi[ ;]") ~= nil then return true end c = string.lower(loader.getenv("beastie_disable") or "") print("beastie_disable", c) return c == "yes" end function menu.autoboot() if menu.already_autoboot then return nil end menu.already_autoboot = true local ab = loader.getenv("autoboot_delay") if ab ~= nil and ab:lower() == "no" then return nil elseif tonumber(ab) == -1 then core.boot() end ab = tonumber(ab) or 10 local x = loader.getenv("loader_menu_timeout_x") or 5 local y = loader.getenv("loader_menu_timeout_y") or 22 local endtime = loader.time() + ab local time repeat time = endtime - loader.time() screen.setcursor(x, y) print("Autoboot in " .. time .. " seconds, hit [Enter] to boot" .. " or any other key to stop ") screen.defcursor() if io.ischar() then local ch = io.getchar() if ch == core.KEY_ENTER then break else -- erase autoboot msg screen.setcursor(0, y) print(" " .. " ") screen.defcursor() return ch end end loader.delay(50000) until time <= 0 core.boot() end return menu