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