xref: /freebsd/stand/lua/config.lua (revision 07c17b2b00d8c1c8a2d58d4d8f99e64ec1182476)
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 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
127-- Module exports
128-- Which variables we changed
129config.env_changed = {}
130-- Values to restore env to (nil to unset)
131config.env_restore = {}
132
133-- The first item in every carousel is always the default item.
134function config.getCarouselIndex(id)
135	local val = carousel_choices[id]
136	if val == nil then
137		return 1
138	end
139	return val
140end
141
142function config.setCarouselIndex(id, idx)
143	carousel_choices[id] = idx
144end
145
146function config.restoreEnv()
147	-- Examine changed environment variables
148	for k, v in pairs(config.env_changed) do
149		local restore_value = config.env_restore[k]
150		if restore_value == nil then
151			-- This one doesn't need restored for some reason
152			goto continue
153		end
154		local current_value = loader.getenv(k)
155		if current_value ~= v then
156			-- This was overwritten by some action taken on the menu
157			-- most likely; we'll leave it be.
158			goto continue
159		end
160		restore_value = restore_value.value
161		if restore_value ~= nil then
162			loader.setenv(k, restore_value)
163		else
164			loader.unsetenv(k)
165		end
166		::continue::
167	end
168
169	config.env_changed = {}
170	config.env_restore = {}
171end
172
173function config.setenv(k, v)
174	-- Track the original value for this if we haven't already
175	if config.env_restore[k] == nil then
176		config.env_restore[k] = {value = loader.getenv(k)}
177	end
178
179	config.env_changed[k] = v
180
181	return loader.setenv(k, v)
182end
183
184function config.setKey(k, n, v)
185	if modules[k] == nil then
186		modules[k] = {}
187	end
188	modules[k][n] = v
189end
190
191function config.lsModules()
192	print("== Listing modules")
193	for k, v in pairs(modules) do
194		print(k, v.load)
195	end
196	print("== List of modules ended")
197end
198
199
200function config.isValidComment(c)
201	if c ~= nil then
202		local s = c:match("^%s*#.*")
203		if s == nil then
204			s = c:match("^%s*$")
205		end
206		if s == nil then
207			return false
208		end
209	end
210	return true
211end
212
213function config.loadmod(mod, silent)
214	local status = true
215	for k, v in pairs(mod) do
216		if v.load == "YES" then
217			local str = "load "
218			if v.flags ~= nil then
219				str = str .. v.flags .. " "
220			end
221			if v.type ~= nil then
222				str = str .. "-t " .. v.type .. " "
223			end
224			if v.name ~= nil then
225				str = str .. v.name
226			else
227				str = str .. k
228			end
229
230			if v.before ~= nil then
231				if loader.perform(v.before) ~= 0 then
232					if not silent then
233						print("Failed to execute '" ..
234						    v.before ..
235						    "' before loading '" .. k ..
236						    "'")
237					end
238					status = false
239				end
240			end
241
242			if loader.perform(str) ~= 0 then
243				if not silent then
244					print("Failed to execute '" .. str ..
245					    "'")
246				end
247				if v.error ~= nil then
248					loader.perform(v.error)
249				end
250				status = false
251			end
252
253			if v.after ~= nil then
254				if loader.perform(v.after) ~= 0 then
255					if not silent then
256						print("Failed to execute '" ..
257						    v.after ..
258						    "' after loading '" .. k ..
259						    "'")
260					end
261					status = false
262				end
263			end
264
265--		else
266--			if not silent then
267--				print("Skipping module '". . k .. "'")
268--			end
269		end
270	end
271
272	return status
273end
274
275-- silent runs will not return false if we fail to open the file
276function config.parse(name, silent)
277	if silent == nil then
278		silent = false
279	end
280	local f = io.open(name)
281	if f == nil then
282		if not silent then
283			print("Failed to open config: '" .. name .. "'")
284		end
285		return silent
286	end
287
288	local text, _ = io.read(f)
289
290	if text == nil then
291		if not silent then
292			print("Failed to read config: '" .. name .. "'")
293		end
294		return silent
295	end
296
297	local n = 1
298	local status = true
299
300	for line in text:gmatch("([^\n]+)") do
301		if line:match("^%s*$") == nil then
302			local found = false
303
304			for _, val in ipairs(pattern_table) do
305				local k, v, c = line:match(val.str)
306				if k ~= nil then
307					found = true
308
309					if config.isValidComment(c) then
310						val.process(k, v)
311					else
312						print("Malformed line (" .. n ..
313						    "):\n\t'" .. line .. "'")
314						status = false
315					end
316
317					break
318				end
319			end
320
321			if not found then
322				print("Malformed line (" .. n .. "):\n\t'" ..
323				    line .. "'")
324				status = false
325			end
326		end
327		n = n + 1
328	end
329
330	return status
331end
332
333-- other_kernel is optionally the name of a kernel to load, if not the default
334-- or autoloaded default from the module_path
335function config.loadkernel(other_kernel)
336	local flags = loader.getenv("kernel_options") or ""
337	local kernel = other_kernel or loader.getenv("kernel")
338
339	local function try_load(names)
340		for name in names:gmatch("([^;]+)%s*;?") do
341			local r = loader.perform("load " .. flags ..
342			    " " .. name)
343			if r == 0 then
344				return name
345			end
346		end
347		return nil
348	end
349
350	local function load_bootfile()
351		local bootfile = loader.getenv("bootfile")
352
353		-- append default kernel name
354		if bootfile == nil then
355			bootfile = "kernel"
356		else
357			bootfile = bootfile .. ";kernel"
358		end
359
360		return try_load(bootfile)
361	end
362
363	-- kernel not set, try load from default module_path
364	if kernel == nil then
365		local res = load_bootfile()
366
367		if res ~= nil then
368			-- Default kernel is loaded
369			config.kernel_loaded = nil
370			return true
371		else
372			print("No kernel set, failed to load from module_path")
373			return false
374		end
375	else
376		-- Use our cached module_path, so we don't end up with multiple
377		-- automatically added kernel paths to our final module_path
378		local module_path = config.module_path
379		local res
380
381		if other_kernel ~= nil then
382			kernel = other_kernel
383		end
384		-- first try load kernel with module_path = /boot/${kernel}
385		-- then try load with module_path=${kernel}
386		local paths = {"/boot/" .. kernel, kernel}
387
388		for _, v in pairs(paths) do
389			loader.setenv("module_path", v)
390			res = load_bootfile()
391
392			-- succeeded, add path to module_path
393			if res ~= nil then
394				config.kernel_loaded = kernel
395				if module_path ~= nil then
396					loader.setenv("module_path", v .. ";" ..
397					    module_path)
398				end
399				return true
400			end
401		end
402
403		-- failed to load with ${kernel} as a directory
404		-- try as a file
405		res = try_load(kernel)
406		if res ~= nil then
407			config.kernel_loaded = kernel
408			return true
409		else
410			print("Failed to load kernel '" .. kernel .. "'")
411			return false
412		end
413	end
414end
415
416function config.selectkernel(kernel)
417	config.kernel_selected = kernel
418end
419
420function config.load(file)
421	if not file then
422		file = "/boot/defaults/loader.conf"
423	end
424
425	if not config.parse(file) then
426		print("Failed to parse configuration: '" .. file .. "'")
427	end
428
429	local f = loader.getenv("loader_conf_files")
430	if f ~= nil then
431		for name in f:gmatch("([%w%p]+)%s*") do
432			-- These may or may not exist, and that's ok. Do a
433			-- silent parse so that we complain on parse errors but
434			-- not for them simply not existing.
435			if not config.parse(name, true) then
436				print("Failed to parse configuration: '" ..
437				    name .. "'")
438			end
439		end
440	end
441
442	-- Cache the provided module_path at load time for later use
443	config.module_path = loader.getenv("module_path")
444end
445
446-- Reload configuration
447function config.reload(file)
448	modules = {}
449	config.restoreEnv()
450	config.load(file)
451end
452
453function config.loadelf()
454	local kernel = config.kernel_selected or config.kernel_loaded
455	local loaded
456
457	print("Loading kernel...")
458	loaded = config.loadkernel(kernel)
459
460	if not loaded then
461		print("Failed to load any kernel")
462		return
463	end
464
465	print("Loading configured modules...")
466	if not config.loadmod(modules) then
467		print("Could not load one or more modules!")
468	end
469end
470
471return config
472