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(k, v) 232 -- Track the original value for this if we haven't already 233 if config.env_restore[k] == nil then 234 config.env_restore[k] = {value = loader.getenv(k)} 235 end 236 237 config.env_changed[k] = v 238 239 return loader.setenv(k, v) 240end 241 242function config.setKey(k, n, v) 243 if modules[k] == nil then 244 modules[k] = {} 245 end 246 modules[k][n] = v 247end 248 249function config.lsModules() 250 print("== Listing modules") 251 for k, v in pairs(modules) do 252 print(k, v.load) 253 end 254 print("== List of modules ended") 255end 256 257 258function config.isValidComment(c) 259 if c ~= nil then 260 local s = c:match("^%s*#.*") 261 if s == nil then 262 s = c:match("^%s*$") 263 end 264 if s == nil then 265 return false 266 end 267 end 268 return true 269end 270 271function config.loadmod(mod, silent) 272 local status = true 273 for k, v in pairs(mod) do 274 if v.load == "YES" then 275 local str = "load " 276 if v.flags ~= nil then 277 str = str .. v.flags .. " " 278 end 279 if v.type ~= nil then 280 str = str .. "-t " .. v.type .. " " 281 end 282 if v.name ~= nil then 283 str = str .. v.name 284 else 285 str = str .. k 286 end 287 288 if v.before ~= nil then 289 if loader.perform(v.before) ~= 0 then 290 if not silent then 291 print("Failed to execute '" .. 292 v.before .. 293 "' before loading '" .. k .. 294 "'") 295 end 296 status = false 297 end 298 end 299 300 if loader.perform(str) ~= 0 then 301 if not silent then 302 print("Failed to execute '" .. str .. 303 "'") 304 end 305 if v.error ~= nil then 306 loader.perform(v.error) 307 end 308 status = false 309 end 310 311 if v.after ~= nil then 312 if loader.perform(v.after) ~= 0 then 313 if not silent then 314 print("Failed to execute '" .. 315 v.after .. 316 "' after loading '" .. k .. 317 "'") 318 end 319 status = false 320 end 321 end 322 323-- else 324-- if not silent then 325-- print("Skipping module '". . k .. "'") 326-- end 327 end 328 end 329 330 return status 331end 332 333function config.processFile(name, silent) 334 if silent == nil then 335 silent = false 336 end 337 338 local text = readFile(name, silent) 339 if text == nil then 340 return not silent 341 end 342 343 return config.parse(text) 344end 345 346-- silent runs will not return false if we fail to open the file 347-- check_and_halt, if it's set, will be executed on the full text of the config 348-- file. If it returns false, we are to halt immediately. 349function config.parse(text) 350 local n = 1 351 local status = true 352 353 for line in text:gmatch("([^\n]+)") do 354 if line:match("^%s*$") == nil then 355 local found = false 356 357 for _, val in ipairs(pattern_table) do 358 local k, v, c = line:match(val.str) 359 if k ~= nil then 360 found = true 361 362 if config.isValidComment(c) then 363 val.process(k, v) 364 else 365 print("Malformed line (" .. n .. 366 "):\n\t'" .. line .. "'") 367 status = false 368 end 369 370 break 371 end 372 end 373 374 if not found then 375 print("Malformed line (" .. n .. "):\n\t'" .. 376 line .. "'") 377 status = false 378 end 379 end 380 n = n + 1 381 end 382 383 return status 384end 385 386-- other_kernel is optionally the name of a kernel to load, if not the default 387-- or autoloaded default from the module_path 388function config.loadKernel(other_kernel) 389 local flags = loader.getenv("kernel_options") or "" 390 local kernel = other_kernel or loader.getenv("kernel") 391 392 local function tryLoad(names) 393 for name in names:gmatch("([^;]+)%s*;?") do 394 local r = loader.perform("load " .. flags .. 395 " " .. name) 396 if r == 0 then 397 return name 398 end 399 end 400 return nil 401 end 402 403 local function loadBootfile() 404 local bootfile = loader.getenv("bootfile") 405 406 -- append default kernel name 407 if bootfile == nil then 408 bootfile = "kernel" 409 else 410 bootfile = bootfile .. ";kernel" 411 end 412 413 return tryLoad(bootfile) 414 end 415 416 -- kernel not set, try load from default module_path 417 if kernel == nil then 418 local res = loadBootfile() 419 420 if res ~= nil then 421 -- Default kernel is loaded 422 config.kernel_loaded = nil 423 return true 424 else 425 print("No kernel set, failed to load from module_path") 426 return false 427 end 428 else 429 -- Use our cached module_path, so we don't end up with multiple 430 -- automatically added kernel paths to our final module_path 431 local module_path = config.module_path 432 local res 433 434 if other_kernel ~= nil then 435 kernel = other_kernel 436 end 437 -- first try load kernel with module_path = /boot/${kernel} 438 -- then try load with module_path=${kernel} 439 local paths = {"/boot/" .. kernel, kernel} 440 441 for _, v in pairs(paths) do 442 loader.setenv("module_path", v) 443 res = loadBootfile() 444 445 -- succeeded, add path to module_path 446 if res ~= nil then 447 config.kernel_loaded = kernel 448 if module_path ~= nil then 449 loader.setenv("module_path", v .. ";" .. 450 module_path) 451 end 452 return true 453 end 454 end 455 456 -- failed to load with ${kernel} as a directory 457 -- try as a file 458 res = tryLoad(kernel) 459 if res ~= nil then 460 config.kernel_loaded = kernel 461 return true 462 else 463 print("Failed to load kernel '" .. kernel .. "'") 464 return false 465 end 466 end 467end 468 469function config.selectKernel(kernel) 470 config.kernel_selected = kernel 471end 472 473function config.load(file) 474 if not file then 475 file = "/boot/defaults/loader.conf" 476 end 477 478 if not config.processFile(file) then 479 print("Failed to parse configuration: '" .. file .. "'") 480 end 481 482 local f = loader.getenv("loader_conf_files") 483 if f ~= nil then 484 for name in f:gmatch("([%w%p]+)%s*") do 485 -- These may or may not exist, and that's ok. Do a 486 -- silent parse so that we complain on parse errors but 487 -- not for them simply not existing. 488 if not config.processFile(name, true) then 489 print("Failed to parse configuration: '" .. 490 name .. "'") 491 end 492 end 493 end 494 495 checkNextboot() 496 497 -- Cache the provided module_path at load time for later use 498 config.module_path = loader.getenv("module_path") 499end 500 501-- Reload configuration 502function config.reload(file) 503 modules = {} 504 config.restoreEnv() 505 config.load(file) 506end 507 508function config.loadelf() 509 local kernel = config.kernel_selected or config.kernel_loaded 510 local loaded 511 512 print("Loading kernel...") 513 loaded = config.loadKernel(kernel) 514 515 if not loaded then 516 print("Failed to load any kernel") 517 return 518 end 519 520 print("Loading configured modules...") 521 if not config.loadmod(modules) then 522 print("Could not load one or more modules!") 523 end 524end 525 526return config 527