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