diff --git a/libexec/flua/modules/lfs.c b/libexec/flua/modules/lfs.c index 52a30c1515a9..e36c78d3b35b 100644 --- a/libexec/flua/modules/lfs.c +++ b/libexec/flua/modules/lfs.c @@ -1,424 +1,450 @@ /*- * Copyright (c) 2018 Conrad Meyer * 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. * * Portions derived from https://github.com/keplerproject/luafilesystem under * the terms of the MIT license: * * Copyright (c) 2003-2014 Kepler Project. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, * modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include __FBSDID("$FreeBSD$"); #ifndef _STANDALONE #include #include #include #include #include #include #endif #include #include "lauxlib.h" #include "lfs.h" #ifdef _STANDALONE #include "lstd.h" #include "lutils.h" #include "bootstrap.h" #endif #ifndef nitems #define nitems(x) (sizeof((x)) / sizeof((x)[0])) #endif /* * The goal is to emulate a subset of the upstream Lua FileSystem library, as * faithfully as possible in the boot environment. Only APIs that seem useful * need to emulated. * * Example usage: * * for file in lfs.dir("/boot") do * print("\t"..file) * end * * Prints: * . * .. * (etc.) * * The other available API is lfs.attributes(), which functions somewhat like * stat(2) and returns a table of values. Example code: * * attrs, errormsg, errorcode = lfs.attributes("/boot") * if attrs == nil then * print(errormsg) * return errorcode * end * * for k, v in pairs(attrs) do * print(k .. ":\t" .. v) * end * return 0 * * Prints (on success): * gid: 0 * change: 140737488342640 * mode: directory * rdev: 0 * ino: 4199275 * dev: 140737488342544 * modification: 140737488342576 * size: 512 * access: 140737488342560 * permissions: 755 * nlink: 58283552 * uid: 1001 */ #define DIR_METATABLE "directory iterator metatable" +static int +lua_dir_iter_pushtype(lua_State *L __unused, const struct dirent *ent __unused) +{ + + /* + * This is a non-standard extension to luafilesystem for loader's + * benefit. The extra stat() calls to determine the entry type can + * be quite expensive on some systems, so this speeds up enumeration of + * /boot greatly by providing the type up front. + * + * This extension is compatible enough with luafilesystem, in that we're + * just using an extra return value for the iterator. + */ +#ifdef _STANDALONE + lua_pushinteger(L, ent->d_type); + return 1; +#else + return 0; +#endif +} + static int lua_dir_iter_next(lua_State *L) { struct dirent *entry; DIR *dp, **dpp; dpp = (DIR **)luaL_checkudata(L, 1, DIR_METATABLE); dp = *dpp; luaL_argcheck(L, dp != NULL, 1, "closed directory"); #ifdef _STANDALONE entry = readdirfd(dp->fd); #else entry = readdir(dp); #endif if (entry == NULL) { closedir(dp); *dpp = NULL; return 0; } lua_pushstring(L, entry->d_name); - return 1; + return 1 + lua_dir_iter_pushtype(L, entry); } static int lua_dir_iter_close(lua_State *L) { DIR *dp, **dpp; dpp = (DIR **)lua_touserdata(L, 1); dp = *dpp; if (dp == NULL) return 0; closedir(dp); *dpp = NULL; return 0; } static int lua_dir(lua_State *L) { const char *path; DIR *dp; if (lua_gettop(L) != 1) { lua_pushnil(L); return 1; } path = luaL_checkstring(L, 1); dp = opendir(path); if (dp == NULL) { lua_pushnil(L); return 1; } lua_pushcfunction(L, lua_dir_iter_next); *(DIR **)lua_newuserdata(L, sizeof(DIR **)) = dp; luaL_getmetatable(L, DIR_METATABLE); lua_setmetatable(L, -2); return 2; } static void register_metatable(lua_State *L) { /* * Create so-called metatable for iterator object returned by * lfs.dir(). */ luaL_newmetatable(L, DIR_METATABLE); lua_newtable(L); lua_pushcfunction(L, lua_dir_iter_next); lua_setfield(L, -2, "next"); lua_pushcfunction(L, lua_dir_iter_close); lua_setfield(L, -2, "close"); /* Magically associate anonymous method table with metatable. */ lua_setfield(L, -2, "__index"); /* Implement magic destructor method */ lua_pushcfunction(L, lua_dir_iter_close); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); } #define PUSH_INTEGER(lname, stname) \ static void \ push_st_ ## lname (lua_State *L, struct stat *sb) \ { \ lua_pushinteger(L, (lua_Integer)sb->st_ ## stname); \ } PUSH_INTEGER(dev, dev) PUSH_INTEGER(ino, ino) PUSH_INTEGER(nlink, nlink) PUSH_INTEGER(uid, uid) PUSH_INTEGER(gid, gid) PUSH_INTEGER(rdev, rdev) PUSH_INTEGER(access, atime) PUSH_INTEGER(modification, mtime) PUSH_INTEGER(change, ctime) PUSH_INTEGER(size, size) #undef PUSH_INTEGER static void push_st_mode(lua_State *L, struct stat *sb) { const char *mode_s; mode_t mode; mode = (sb->st_mode & S_IFMT); if (S_ISREG(mode)) mode_s = "file"; else if (S_ISDIR(mode)) mode_s = "directory"; else if (S_ISLNK(mode)) mode_s = "link"; else if (S_ISSOCK(mode)) mode_s = "socket"; else if (S_ISFIFO(mode)) mode_s = "fifo"; else if (S_ISCHR(mode)) mode_s = "char device"; else if (S_ISBLK(mode)) mode_s = "block device"; else mode_s = "other"; lua_pushstring(L, mode_s); } static void push_st_permissions(lua_State *L, struct stat *sb) { char buf[20]; /* * XXX * Could actually format as "-rwxrwxrwx" -- do we care? */ snprintf(buf, sizeof(buf), "%o", sb->st_mode & ~S_IFMT); lua_pushstring(L, buf); } #define PUSH_ENTRY(n) { #n, push_st_ ## n } struct stat_members { const char *name; void (*push)(lua_State *, struct stat *); } members[] = { PUSH_ENTRY(mode), PUSH_ENTRY(dev), PUSH_ENTRY(ino), PUSH_ENTRY(nlink), PUSH_ENTRY(uid), PUSH_ENTRY(gid), PUSH_ENTRY(rdev), PUSH_ENTRY(access), PUSH_ENTRY(modification), PUSH_ENTRY(change), PUSH_ENTRY(size), PUSH_ENTRY(permissions), }; #undef PUSH_ENTRY static int lua_attributes(lua_State *L) { struct stat sb; const char *path, *member; size_t i; int rc; path = luaL_checkstring(L, 1); if (path == NULL) { lua_pushnil(L); lua_pushfstring(L, "cannot convert first argument to string"); lua_pushinteger(L, EINVAL); return 3; } rc = stat(path, &sb); if (rc != 0) { lua_pushnil(L); lua_pushfstring(L, "cannot obtain information from file '%s': %s", path, strerror(errno)); lua_pushinteger(L, errno); return 3; } if (lua_isstring(L, 2)) { member = lua_tostring(L, 2); for (i = 0; i < nitems(members); i++) { if (strcmp(members[i].name, member) != 0) continue; members[i].push(L, &sb); return 1; } return luaL_error(L, "invalid attribute name '%s'", member); } /* Create or reuse existing table */ lua_settop(L, 2); if (!lua_istable(L, 2)) lua_newtable(L); /* Export all stat data to caller */ for (i = 0; i < nitems(members); i++) { lua_pushstring(L, members[i].name); members[i].push(L, &sb); lua_rawset(L, -3); } return 1; } #ifndef _STANDALONE #define lfs_mkdir_impl(path) (mkdir((path), \ S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | \ S_IROTH | S_IXOTH)) static int lua_mkdir(lua_State *L) { const char *path; int error, serrno; path = luaL_checkstring(L, 1); if (path == NULL) { lua_pushnil(L); lua_pushfstring(L, "cannot convert first argument to string"); lua_pushinteger(L, EINVAL); return 3; } error = lfs_mkdir_impl(path); if (error == -1) { /* Save it; unclear what other libc functions may be invoked */ serrno = errno; lua_pushnil(L); lua_pushfstring(L, strerror(serrno)); lua_pushinteger(L, serrno); return 3; } lua_pushboolean(L, 1); return 1; } static int lua_rmdir(lua_State *L) { const char *path; int error, serrno; path = luaL_checkstring(L, 1); if (path == NULL) { lua_pushnil(L); lua_pushfstring(L, "cannot convert first argument to string"); lua_pushinteger(L, EINVAL); return 3; } error = rmdir(path); if (error == -1) { /* Save it; unclear what other libc functions may be invoked */ serrno = errno; lua_pushnil(L); lua_pushfstring(L, strerror(serrno)); lua_pushinteger(L, serrno); return 3; } lua_pushboolean(L, 1); return 1; } #endif #define REG_SIMPLE(n) { #n, lua_ ## n } static const struct luaL_Reg fslib[] = { REG_SIMPLE(attributes), REG_SIMPLE(dir), #ifndef _STANDALONE REG_SIMPLE(mkdir), REG_SIMPLE(rmdir), #endif { NULL, NULL }, }; #undef REG_SIMPLE int luaopen_lfs(lua_State *L) { register_metatable(L); luaL_newlib(L, fslib); +#ifdef _STANDALONE + /* Non-standard extension for loader, used with lfs.dir(). */ + lua_pushinteger(L, DT_DIR); + lua_setfield(L, -2, "DT_DIR"); +#endif return 1; } diff --git a/stand/lua/core.lua b/stand/lua/core.lua index 9d331bc0ad3a..a119c3c258f8 100644 --- a/stand/lua/core.lua +++ b/stand/lua/core.lua @@ -1,503 +1,507 @@ -- -- SPDX-License-Identifier: BSD-2-Clause-FreeBSD -- -- 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 config = require("config") local hook = require("hook") local core = {} local default_safe_mode = false local default_single_user = false local default_verbose = false local bootenv_list = "bootenvs" local function composeLoaderCmd(cmd_name, argstr) if argstr ~= nil then cmd_name = cmd_name .. " " .. argstr end return cmd_name end local function recordDefaults() -- 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. local boot_acpi = core.isSystem386() and core.getACPIPresent(false) local boot_single = loader.getenv("boot_single") or "no" local boot_verbose = loader.getenv("boot_verbose") or "no" default_single_user = boot_single:lower() ~= "no" default_verbose = boot_verbose:lower() ~= "no" if boot_acpi then core.setACPI(true) end core.setSingleUser(default_single_user) core.setVerbose(default_verbose) end -- Globals -- try_include will return the loaded module on success, or false and the error -- message on failure. function try_include(module) if module:sub(1, 1) ~= "/" then local lua_path = loader.lua_path -- XXX Temporary compat shim; this should be removed once the -- loader.lua_path export has sufficiently spread. if lua_path == nil then lua_path = "/boot/lua" end module = lua_path .. "/" .. module -- We only attempt to append an extension if an absolute path -- wasn't specified. This assumes that the caller either wants -- to treat this like it would require() and specify just the -- base filename, or they know what they're doing as they've -- specified an absolute path and we shouldn't impede. if module:match(".lua$") == nil then module = module .. ".lua" end end if lfs.attributes(module, "mode") ~= "file" then return end return dofile(module) end -- Module exports -- Commonly appearing constants core.KEY_BACKSPACE = 8 core.KEY_ENTER = 13 core.KEY_DELETE = 127 -- Note that this is a decimal representation, despite the leading 0 that in -- other contexts (outside of Lua) may mean 'octal' core.KEYSTR_ESCAPE = "\027" core.KEYSTR_CSI = core.KEYSTR_ESCAPE .. "[" core.KEYSTR_RESET = core.KEYSTR_ESCAPE .. "c" 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(verbose) if verbose == nil then verbose = not core.verbose end if verbose then loader.setenv("boot_verbose", "YES") else loader.unsetenv("boot_verbose") end core.verbose = verbose end function core.setSingleUser(single_user) if single_user == nil then single_user = not core.su end if single_user then loader.setenv("boot_single", "YES") else loader.unsetenv("boot_single") end core.su = single_user end function core.getACPIPresent(checking_system_defaults) local c = loader.getenv("hint.acpi.0.rsdp") if c ~= nil then if checking_system_defaults 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(acpi) if acpi == nil then acpi = not core.acpi end if acpi 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 = acpi end function core.setSafeMode(safe_mode) if safe_mode == nil then safe_mode = not core.sm end if safe_mode 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 = safe_mode end function core.clearCachedKernels() -- Clear the kernel cache on config changes, autodetect might have -- changed or if we've switched boot environments then we could have -- a new kernel set. core.cached_kernels = nil end function core.kernelList() if core.cached_kernels ~= nil then return core.cached_kernels end local k = loader.getenv("kernel") local v = loader.getenv("kernels") local autodetect = loader.getenv("kernels_autodetect") or "" 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 end -- Base whether we autodetect kernels or not on a loader.conf(5) -- setting, kernels_autodetect. If it's set to 'yes', we'll add -- any kernels we detect based on the criteria described. if autodetect:lower() ~= "yes" then core.cached_kernels = kernels return core.cached_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 + for file, ftype in lfs.dir("/boot") do local fname = "/boot/" .. file if file == "." or file == ".." then goto continue end - if lfs.attributes(fname, "mode") ~= "directory" then + if ftype then + if ftype ~= lfs.DT_DIR then + goto continue + end + elseif 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 core.cached_kernels = kernels return core.cached_kernels end function core.bootenvDefault() return loader.getenv("zfs_be_active") end function core.bootenvList() local bootenv_count = tonumber(loader.getenv(bootenv_list .. "_count")) local bootenvs = {} local curenv local envcount = 0 local unique = {} if bootenv_count == nil or bootenv_count <= 0 then return bootenvs end -- Currently selected bootenv is always first/default -- On the rewinded list the bootenv may not exists if core.isRewinded() then curenv = core.bootenvDefaultRewinded() else curenv = core.bootenvDefault() end 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(bootenv_list .. "[" .. 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.isCheckpointed() return loader.getenv("zpool_checkpoint") ~= nil end function core.bootenvDefaultRewinded() local defname = "zfs:!" .. string.sub(core.bootenvDefault(), 5) local bootenv_count = tonumber("bootenvs_check_count") if bootenv_count == nil or bootenv_count <= 0 then return defname end for curenv_idx = 0, bootenv_count - 1 do local curenv = loader.getenv("bootenvs_check[" .. curenv_idx .. "]") if curenv == defname then return defname end end return loader.getenv("bootenvs_check[0]") end function core.isRewinded() return bootenv_list == "bootenvs_check" end function core.changeRewindCheckpoint() if core.isRewinded() then bootenv_list = "bootenvs" else bootenv_list = "bootenvs_check" end end function core.setDefaults() core.setACPI(core.getACPIPresent(true)) core.setSafeMode(default_safe_mode) core.setSingleUser(default_single_user) core.setVerbose(default_verbose) end function core.autoboot(argstr) -- loadelf() only if we've not already loaded a kernel if loader.getenv("kernelname") == nil then config.loadelf() end loader.perform(composeLoaderCmd("autoboot", argstr)) end function core.boot(argstr) -- loadelf() only if we've not already loaded a kernel if loader.getenv("kernelname") == nil then config.loadelf() end loader.perform(composeLoaderCmd("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.isUEFIBoot() local efiver = loader.getenv("efi-version") return efiver ~= nil end function core.isZFSBoot() local c = loader.getenv("currdev") if c ~= nil then return c:match("^zfs:") ~= nil end return false end function core.isFramebufferConsole() local c = loader.getenv("console") if c ~= nil then if c:find("efi") == nil and c:find("vidconsole") == nil then return false end if loader.getenv("screen.depth") ~= nil then return true end end return false end function core.isSerialConsole() local c = loader.getenv("console") if c ~= nil then if c:find("comconsole") ~= nil then return true end end return false end function core.isSerialBoot() 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 -- Is the menu skipped in the environment in which we've booted? function core.isMenuSkipped() return string.lower(loader.getenv("beastie_disable") or "") == "yes" end -- This may be a better candidate for a 'utility' module. function core.deepCopyTable(tbl) local new_tbl = {} for k, v in pairs(tbl) do if type(v) == "table" then new_tbl[k] = core.deepCopyTable(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 function core.getConsoleName() if loader.getenv("boot_multicons") ~= nil then if loader.getenv("boot_serial") ~= nil then return "Dual (Serial primary)" else return "Dual (Video primary)" end else if loader.getenv("boot_serial") ~= nil then return "Serial" else return "Video" end end end function core.nextConsoleChoice() if loader.getenv("boot_multicons") ~= nil then if loader.getenv("boot_serial") ~= nil then loader.unsetenv("boot_serial") else loader.unsetenv("boot_multicons") loader.setenv("boot_serial", "YES") end else if loader.getenv("boot_serial") ~= nil then loader.unsetenv("boot_serial") else loader.setenv("boot_multicons", "YES") loader.setenv("boot_serial", "YES") end end end recordDefaults() hook.register("config.reloaded", core.clearCachedKernels) return core