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 config = {} 33 34local modules = {} 35 36local carousel_choices = {} 37 38local pattern_table = { 39 { 40 str = "^%s*(#.*)", 41 process = function(_, _) end, 42 }, 43 -- module_load="value" 44 { 45 str = "^%s*([%w_]+)_load%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 46 process = function(k, v) 47 if modules[k] == nil then 48 modules[k] = {} 49 end 50 modules[k].load = v:upper() 51 end, 52 }, 53 -- module_name="value" 54 { 55 str = "^%s*([%w_]+)_name%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 56 process = function(k, v) 57 config.setKey(k, "name", v) 58 end, 59 }, 60 -- module_type="value" 61 { 62 str = "^%s*([%w_]+)_type%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 63 process = function(k, v) 64 config.setKey(k, "type", v) 65 end, 66 }, 67 -- module_flags="value" 68 { 69 str = "^%s*([%w_]+)_flags%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 70 process = function(k, v) 71 config.setKey(k, "flags", v) 72 end, 73 }, 74 -- module_before="value" 75 { 76 str = "^%s*([%w_]+)_before%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 77 process = function(k, v) 78 config.setKey(k, "before", v) 79 end, 80 }, 81 -- module_after="value" 82 { 83 str = "^%s*([%w_]+)_after%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 84 process = function(k, v) 85 config.setKey(k, "after", v) 86 end, 87 }, 88 -- module_error="value" 89 { 90 str = "^%s*([%w_]+)_error%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 91 process = function(k, v) 92 config.setKey(k, "error", v) 93 end, 94 }, 95 -- exec="command" 96 { 97 str = "^%s*exec%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 98 process = function(k, _) 99 if loader.perform(k) ~= 0 then 100 print("Failed to exec '" .. k .. "'") 101 end 102 end, 103 }, 104 -- env_var="value" 105 { 106 str = "^%s*([%w%p]+)%s*=%s*\"([%w%s%p]-)\"%s*(.*)", 107 process = function(k, v) 108 if config.setenv(k, v) ~= 0 then 109 print("Failed to set '" .. k .. 110 "' with value: " .. v .. "") 111 end 112 end, 113 }, 114 -- env_var=num 115 { 116 str = "^%s*([%w%p]+)%s*=%s*(%d+)%s*(.*)", 117 process = function(k, v) 118 if config.setenv(k, v) ~= 0 then 119 print("Failed to set '" .. k .. 120 "' with value: " .. v .. "") 121 end 122 end, 123 }, 124} 125 126local function readFile(name, silent) 127 local f = io.open(name) 128 if f == nil then 129 if not silent then 130 print("Failed to open config: '" .. name .. "'") 131 end 132 return nil 133 end 134 135 local text, _ = io.read(f) 136 -- We might have read in the whole file, this won't be needed any more. 137 io.close(f) 138 139 if text == nil then 140 if not silent then 141 print("Failed to read config: '" .. name .. "'") 142 end 143 return nil 144 end 145 return text 146end 147 148local function checkNextboot() 149 local nextboot_file = loader.getenv("nextboot_file") 150 if nextboot_file == nil then 151 return 152 end 153 154 local text = readFile(nextboot_file, true) 155 if text == nil then 156 return 157 end 158 159 if text:match("^nextboot_enable=\"NO\"") ~= nil then 160 -- We're done; nextboot is not enabled 161 return 162 end 163 164 if not config.parse(text) then 165 print("Failed to parse nextboot configuration: '" .. 166 nextboot_file .. "'") 167 end 168 169 -- Attempt to rewrite the first line and only the first line of the 170 -- nextboot_file. We overwrite it with nextboot_enable="NO", then 171 -- check for that on load. 172 -- It's worth noting that this won't work on every filesystem, so we 173 -- won't do anything notable if we have any errors in this process. 174 local nfile = io.open(nextboot_file, 'w') 175 if nfile ~= nil then 176 -- We need the trailing space here to account for the extra 177 -- character taken up by the string nextboot_enable="YES" 178 -- Or new end quotation mark lands on the S, and we want to 179 -- rewrite the entirety of the first line. 180 io.write(nfile, "nextboot_enable=\"NO\" ") 181 io.close(nfile) 182 end 183end 184 185-- Module exports 186-- Which variables we changed 187config.env_changed = {} 188-- Values to restore env to (nil to unset) 189config.env_restore = {} 190 191-- The first item in every carousel is always the default item. 192function config.getCarouselIndex(id) 193 local val = carousel_choices[id] 194 if val == nil then 195 return 1 196 end 197 return val 198end 199 200function config.setCarouselIndex(id, idx) 201 carousel_choices[id] = idx 202end 203 204function config.restoreEnv() 205 -- Examine changed environment variables 206 for k, v in pairs(config.env_changed) do 207 local restore_value = config.env_restore[k] 208 if restore_value == nil then 209 -- This one doesn't need restored for some reason 210 goto continue 211 end 212 local current_value = loader.getenv(k) 213 if current_value ~= v then 214 -- This was overwritten by some action taken on the menu 215 -- most likely; we'll leave it be. 216 goto continue 217 end 218 restore_value = restore_value.value 219 if restore_value ~= nil then 220 loader.setenv(k, restore_value) 221 else 222 loader.unsetenv(k) 223 end 224 ::continue:: 225 end 226 227 config.env_changed = {} 228 config.env_restore = {} 229end 230 231function config.setenv(key, value) 232 -- Track the original value for this if we haven't already 233 if config.env_restore[key] == nil then 234 config.env_restore[key] = {value = loader.getenv(key)} 235 end 236 237 config.env_changed[key] = value 238 239 return loader.setenv(key, value) 240end 241 242-- name here is one of 'name', 'type', flags', 'before', 'after', or 'error.' 243-- These are set from lines in loader.conf(5): ${key}_${name}="${value}" where 244-- ${key} is a module name. 245function config.setKey(key, name, value) 246 if modules[key] == nil then 247 modules[key] = {} 248 end 249 modules[key][name] = value 250end 251 252function config.lsModules() 253 print("== Listing modules") 254 for k, v in pairs(modules) do 255 print(k, v.load) 256 end 257 print("== List of modules ended") 258end 259 260 261function config.isValidComment(line) 262 if line ~= nil then 263 local s = line:match("^%s*#.*") 264 if s == nil then 265 s = line:match("^%s*$") 266 end 267 if s == nil then 268 return false 269 end 270 end 271 return true 272end 273 274function config.loadmod(mod, silent) 275 local status = true 276 for k, v in pairs(mod) do 277 if v.load == "YES" then 278 local str = "load " 279 if v.flags ~= nil then 280 str = str .. v.flags .. " " 281 end 282 if v.type ~= nil then 283 str = str .. "-t " .. v.type .. " " 284 end 285 if v.name ~= nil then 286 str = str .. v.name 287 else 288 str = str .. k 289 end 290 291 if v.before ~= nil then 292 if loader.perform(v.before) ~= 0 then 293 if not silent then 294 print("Failed to execute '" .. 295 v.before .. 296 "' before loading '" .. k .. 297 "'") 298 end 299 status = false 300 end 301 end 302 303 if loader.perform(str) ~= 0 then 304 if not silent then 305 print("Failed to execute '" .. str .. 306 "'") 307 end 308 if v.error ~= nil then 309 loader.perform(v.error) 310 end 311 status = false 312 end 313 314 if v.after ~= nil then 315 if loader.perform(v.after) ~= 0 then 316 if not silent then 317 print("Failed to execute '" .. 318 v.after .. 319 "' after loading '" .. k .. 320 "'") 321 end 322 status = false 323 end 324 end 325 326-- else 327-- if not silent then 328-- print("Skipping module '". . k .. "'") 329-- end 330 end 331 end 332 333 return status 334end 335 336function config.processFile(name, silent) 337 if silent == nil then 338 silent = false 339 end 340 341 local text = readFile(name, silent) 342 if text == nil then 343 return not silent 344 end 345 346 return config.parse(text) 347end 348 349-- silent runs will not return false if we fail to open the file 350-- check_and_halt, if it's set, will be executed on the full text of the config 351-- file. If it returns false, we are to halt immediately. 352function config.parse(text) 353 local n = 1 354 local status = true 355 356 for line in text:gmatch("([^\n]+)") do 357 if line:match("^%s*$") == nil then 358 local found = false 359 360 for _, val in ipairs(pattern_table) do 361 local k, v, c = line:match(val.str) 362 if k ~= nil then 363 found = true 364 365 if config.isValidComment(c) then 366 val.process(k, v) 367 else 368 print("Malformed line (" .. n .. 369 "):\n\t'" .. line .. "'") 370 status = false 371 end 372 373 break 374 end 375 end 376 377 if not found then 378 print("Malformed line (" .. n .. "):\n\t'" .. 379 line .. "'") 380 status = false 381 end 382 end 383 n = n + 1 384 end 385 386 return status 387end 388 389-- other_kernel is optionally the name of a kernel to load, if not the default 390-- or autoloaded default from the module_path 391function config.loadKernel(other_kernel) 392 local flags = loader.getenv("kernel_options") or "" 393 local kernel = other_kernel or loader.getenv("kernel") 394 395 local function tryLoad(names) 396 for name in names:gmatch("([^;]+)%s*;?") do 397 local r = loader.perform("load " .. flags .. 398 " " .. name) 399 if r == 0 then 400 return name 401 end 402 end 403 return nil 404 end 405 406 local function loadBootfile() 407 local bootfile = loader.getenv("bootfile") 408 409 -- append default kernel name 410 if bootfile == nil then 411 bootfile = "kernel" 412 else 413 bootfile = bootfile .. ";kernel" 414 end 415 416 return tryLoad(bootfile) 417 end 418 419 -- kernel not set, try load from default module_path 420 if kernel == nil then 421 local res = loadBootfile() 422 423 if res ~= nil then 424 -- Default kernel is loaded 425 config.kernel_loaded = nil 426 return true 427 else 428 print("No kernel set, failed to load from module_path") 429 return false 430 end 431 else 432 -- Use our cached module_path, so we don't end up with multiple 433 -- automatically added kernel paths to our final module_path 434 local module_path = config.module_path 435 local res 436 437 if other_kernel ~= nil then 438 kernel = other_kernel 439 end 440 -- first try load kernel with module_path = /boot/${kernel} 441 -- then try load with module_path=${kernel} 442 local paths = {"/boot/" .. kernel, kernel} 443 444 for _, v in pairs(paths) do 445 loader.setenv("module_path", v) 446 res = loadBootfile() 447 448 -- succeeded, add path to module_path 449 if res ~= nil then 450 config.kernel_loaded = kernel 451 if module_path ~= nil then 452 loader.setenv("module_path", v .. ";" .. 453 module_path) 454 end 455 return true 456 end 457 end 458 459 -- failed to load with ${kernel} as a directory 460 -- try as a file 461 res = tryLoad(kernel) 462 if res ~= nil then 463 config.kernel_loaded = kernel 464 return true 465 else 466 print("Failed to load kernel '" .. kernel .. "'") 467 return false 468 end 469 end 470end 471 472function config.selectKernel(kernel) 473 config.kernel_selected = kernel 474end 475 476function config.load(file) 477 if not file then 478 file = "/boot/defaults/loader.conf" 479 end 480 481 if not config.processFile(file) then 482 print("Failed to parse configuration: '" .. file .. "'") 483 end 484 485 local f = loader.getenv("loader_conf_files") 486 if f ~= nil then 487 for name in f:gmatch("([%w%p]+)%s*") do 488 -- These may or may not exist, and that's ok. Do a 489 -- silent parse so that we complain on parse errors but 490 -- not for them simply not existing. 491 if not config.processFile(name, true) then 492 print("Failed to parse configuration: '" .. 493 name .. "'") 494 end 495 end 496 end 497 498 checkNextboot() 499 500 -- Cache the provided module_path at load time for later use 501 config.module_path = loader.getenv("module_path") 502end 503 504-- Reload configuration 505function config.reload(file) 506 modules = {} 507 config.restoreEnv() 508 config.load(file) 509end 510 511function config.loadelf() 512 local kernel = config.kernel_selected or config.kernel_loaded 513 local loaded 514 515 print("Loading kernel...") 516 loaded = config.loadKernel(kernel) 517 518 if not loaded then 519 print("Failed to load any kernel") 520 return 521 end 522 523 print("Loading configured modules...") 524 if not config.loadmod(modules) then 525 print("Could not load one or more modules!") 526 end 527end 528 529return config 530