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