xref: /freebsd-13-stable/stand/lua/config.lua (revision babfcc0aa66256ffa5142a621ee9eba67d564efb)
1--
2-- SPDX-License-Identifier: BSD-2-Clause
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
30local hook = require("hook")
31
32local config = {}
33local modules = {}
34local carousel_choices = {}
35-- Which variables we changed
36local env_changed = {}
37-- Values to restore env to (nil to unset)
38local env_restore = {}
39
40local MSG_FAILDIR = "Failed to load conf dir '%s': not a directory"
41local MSG_FAILEXEC = "Failed to exec '%s'"
42local MSG_FAILSETENV = "Failed to '%s' with value: %s"
43local MSG_FAILOPENCFG = "Failed to open config: '%s'"
44local MSG_FAILREADCFG = "Failed to read config: '%s'"
45local MSG_FAILPARSECFG = "Failed to parse config: '%s'"
46local MSG_FAILPARSEVAR = "Failed to parse variable '%s': %s"
47local MSG_FAILEXBEF = "Failed to execute '%s' before loading '%s'"
48local MSG_FAILEXAF = "Failed to execute '%s' after loading '%s'"
49local MSG_MALFORMED = "Malformed line (%d):\n\t'%s'"
50local MSG_DEFAULTKERNFAIL = "No kernel set, failed to load from module_path"
51local MSG_KERNFAIL = "Failed to load kernel '%s'"
52local MSG_XENKERNFAIL = "Failed to load Xen kernel '%s'"
53local MSG_XENKERNLOADING = "Loading Xen kernel..."
54local MSG_KERNLOADING = "Loading kernel..."
55local MSG_MODLOADING = "Loading configured modules..."
56local MSG_MODBLACKLIST = "Not loading blacklisted module '%s'"
57
58local MSG_FAILSYN_QUOTE = "Stray quote at position '%d'"
59local MSG_FAILSYN_EOLESC = "Stray escape at end of line"
60local MSG_FAILSYN_EOLVAR = "Unescaped $ at end of line"
61local MSG_FAILSYN_BADVAR = "Malformed variable expression at position '%d'"
62
63-- MODULEEXPR should more or less allow the exact same set of characters as the
64-- env_var entries in the pattern table.  This is perhaps a good target for a
65-- little refactoring.
66local MODULEEXPR = '([%w%d-_.]+)'
67local QVALEXPR = '"(.*)"'
68local QVALREPL = QVALEXPR:gsub('%%', '%%%%')
69local WORDEXPR = "([-%w%d][-%w%d_.]*)"
70local WORDREPL = WORDEXPR:gsub('%%', '%%%%')
71
72-- Entries that should never make it into the environment; each one should have
73-- a documented reason for its existence, and these should all be implementation
74-- details of the config module.
75local loader_env_restricted_table = {
76	-- loader_conf_files should be considered write-only, and consumers
77	-- should not rely on any particular value; it's a loader implementation
78	-- detail.  Moreover, it's not a particularly useful variable to have in
79	-- the kenv.  Save the overhead, let it get fetched other ways.
80	loader_conf_files = true,
81}
82
83local function restoreEnv()
84	-- Examine changed environment variables
85	for k, v in pairs(env_changed) do
86		local restore_value = env_restore[k]
87		if restore_value == nil then
88			-- This one doesn't need restored for some reason
89			goto continue
90		end
91		local current_value = loader.getenv(k)
92		if current_value ~= v then
93			-- This was overwritten by some action taken on the menu
94			-- most likely; we'll leave it be.
95			goto continue
96		end
97		restore_value = restore_value.value
98		if restore_value ~= nil then
99			loader.setenv(k, restore_value)
100		else
101			loader.unsetenv(k)
102		end
103		::continue::
104	end
105
106	env_changed = {}
107	env_restore = {}
108end
109
110-- XXX This getEnv/setEnv should likely be exported at some point.  We can save
111-- the call back into loader.getenv for any variable that's been set or
112-- overridden by any loader.conf using this implementation with little overhead
113-- since we're already tracking the values.
114local function getEnv(key)
115	if loader_env_restricted_table[key] ~= nil or
116	    env_changed[key] ~= nil then
117		return env_changed[key]
118	end
119
120	return loader.getenv(key)
121end
122
123local function setEnv(key, value)
124	env_changed[key] = value
125
126	if loader_env_restricted_table[key] ~= nil then
127		return 0
128	end
129
130	-- Track the original value for this if we haven't already
131	if env_restore[key] == nil then
132		env_restore[key] = {value = loader.getenv(key)}
133	end
134
135	return loader.setenv(key, value)
136end
137
138-- name here is one of 'name', 'type', flags', 'before', 'after', or 'error.'
139-- These are set from lines in loader.conf(5): ${key}_${name}="${value}" where
140-- ${key} is a module name.
141local function setKey(key, name, value)
142	if modules[key] == nil then
143		modules[key] = {}
144	end
145	modules[key][name] = value
146end
147
148-- Escapes the named value for use as a literal in a replacement pattern.
149-- e.g. dhcp.host-name gets turned into dhcp%.host%-name to remove the special
150-- meaning.
151local function escapeName(name)
152	return name:gsub("([%p])", "%%%1")
153end
154
155local function processEnvVar(value)
156	local pval, vlen = '', #value
157	local nextpos, vdelim, vinit = 1
158	local vpat
159	for i = 1, vlen do
160		if i < nextpos then
161			goto nextc
162		end
163
164		local c = value:sub(i, i)
165		if c == '\\' then
166			if i == vlen then
167				return nil, MSG_FAILSYN_EOLESC
168			end
169			nextpos = i + 2
170			pval = pval .. value:sub(i + 1, i + 1)
171		elseif c == '"' then
172			return nil, MSG_FAILSYN_QUOTE:format(i)
173		elseif c == "$" then
174			if i == vlen then
175				return nil, MSG_FAILSYN_EOLVAR
176			else
177				if value:sub(i + 1, i + 1) == "{" then
178					-- Skip ${
179					vinit = i + 2
180					vdelim = '}'
181					vpat = "^([^}]+)}"
182				else
183					-- Skip the $
184					vinit = i + 1
185					vdelim = nil
186					vpat = "^([%w][-%w%d_.]*)"
187				end
188
189				local name = value:match(vpat, vinit)
190				if not name then
191					return nil, MSG_FAILSYN_BADVAR:format(i)
192				else
193					nextpos = vinit + #name
194					if vdelim then
195						nextpos = nextpos + 1
196					end
197
198					local repl = loader.getenv(name) or ""
199					pval = pval .. repl
200				end
201			end
202		else
203			pval = pval .. c
204		end
205		::nextc::
206	end
207
208	return pval
209end
210
211local function checkPattern(line, pattern)
212	local function _realCheck(_line, _pattern)
213		return _line:match(_pattern)
214	end
215
216	if pattern:find('$VALUE') then
217		local k, v, c
218		k, v, c = _realCheck(line, pattern:gsub('$VALUE', QVALREPL))
219		if k ~= nil then
220			return k,v, c
221		end
222		return _realCheck(line, pattern:gsub('$VALUE', WORDREPL))
223	else
224		return _realCheck(line, pattern)
225	end
226end
227
228-- str in this table is a regex pattern.  It will automatically be anchored to
229-- the beginning of a line and any preceding whitespace will be skipped.  The
230-- pattern should have no more than two captures patterns, which correspond to
231-- the two parameters (usually 'key' and 'value') that are passed to the
232-- process function.  All trailing characters will be validated.  Any $VALUE
233-- token included in a pattern will be tried first with a quoted value capture
234-- group, then a single-word value capture group.  This is our kludge for Lua
235-- regex not supporting branching.
236--
237-- We have two special entries in this table: the first is the first entry,
238-- a full-line comment.  The second is for 'exec' handling.  Both have a single
239-- capture group, but the difference is that the full-line comment pattern will
240-- match the entire line.  This does not run afoul of the later end of line
241-- validation that we'll do after a match.  However, the 'exec' pattern will.
242-- We document the exceptions with a special 'groups' index that indicates
243-- the number of capture groups, if not two.  We'll use this later to do
244-- validation on the proper entry.
245--
246local pattern_table = {
247	{
248		str = "(#.*)",
249		process = function(_, _)  end,
250		groups = 1,
251	},
252	--  module_load="value"
253	{
254		str = MODULEEXPR .. "_load%s*=%s*$VALUE",
255		process = function(k, v)
256			if modules[k] == nil then
257				modules[k] = {}
258			end
259			modules[k].load = v:upper()
260		end,
261	},
262	--  module_name="value"
263	{
264		str = MODULEEXPR .. "_name%s*=%s*$VALUE",
265		process = function(k, v)
266			setKey(k, "name", v)
267		end,
268	},
269	--  module_type="value"
270	{
271		str = MODULEEXPR .. "_type%s*=%s*$VALUE",
272		process = function(k, v)
273			setKey(k, "type", v)
274		end,
275	},
276	--  module_flags="value"
277	{
278		str = MODULEEXPR .. "_flags%s*=%s*$VALUE",
279		process = function(k, v)
280			setKey(k, "flags", v)
281		end,
282	},
283	--  module_before="value"
284	{
285		str = MODULEEXPR .. "_before%s*=%s*$VALUE",
286		process = function(k, v)
287			setKey(k, "before", v)
288		end,
289	},
290	--  module_after="value"
291	{
292		str = MODULEEXPR .. "_after%s*=%s*$VALUE",
293		process = function(k, v)
294			setKey(k, "after", v)
295		end,
296	},
297	--  module_error="value"
298	{
299		str = MODULEEXPR .. "_error%s*=%s*$VALUE",
300		process = function(k, v)
301			setKey(k, "error", v)
302		end,
303	},
304	--  exec="command"
305	{
306		str = "exec%s*=%s*" .. QVALEXPR,
307		process = function(k, _)
308			if cli_execute_unparsed(k) ~= 0 then
309				print(MSG_FAILEXEC:format(k))
310			end
311		end,
312		groups = 1,
313	},
314	--  env_var="value" or env_var=[word|num]
315	{
316		str = "([%w][%w%d-_.]*)%s*=%s*$VALUE",
317		process = function(k, v)
318			local pv, msg = processEnvVar(v)
319			if not pv then
320				print(MSG_FAILPARSEVAR:format(k, msg))
321				return
322			end
323			if setEnv(k, pv) ~= 0 then
324				print(MSG_FAILSETENV:format(k, v))
325			end
326		end,
327	},
328}
329
330local function isValidComment(line)
331	if line ~= nil then
332		local s = line:match("^%s*#.*")
333		if s == nil then
334			s = line:match("^%s*$")
335		end
336		if s == nil then
337			return false
338		end
339	end
340	return true
341end
342
343local function getBlacklist()
344	local blacklist = {}
345	local blacklist_str = loader.getenv('module_blacklist')
346	if blacklist_str == nil then
347		return blacklist
348	end
349
350	for mod in blacklist_str:gmatch("[;, ]?([-%w_]+)[;, ]?") do
351		blacklist[mod] = true
352	end
353	return blacklist
354end
355
356local function loadModule(mod, silent)
357	local status = true
358	local blacklist = getBlacklist()
359	local pstatus
360	for k, v in pairs(mod) do
361		if v.load ~= nil and v.load:lower() == "yes" then
362			local module_name = v.name or k
363			if not v.force and blacklist[module_name] ~= nil then
364				if not silent then
365					print(MSG_MODBLACKLIST:format(module_name))
366				end
367				goto continue
368			end
369			if not silent then
370				loader.printc(module_name .. "...")
371			end
372			local str = "load "
373			if v.type ~= nil then
374				str = str .. "-t " .. v.type .. " "
375			end
376			str = str .. module_name
377			if v.flags ~= nil then
378				str = str .. " " .. v.flags
379			end
380			if v.before ~= nil then
381				pstatus = cli_execute_unparsed(v.before) == 0
382				if not pstatus and not silent then
383					print(MSG_FAILEXBEF:format(v.before, k))
384				end
385				status = status and pstatus
386			end
387
388			if cli_execute_unparsed(str) ~= 0 then
389				-- XXX Temporary shim: don't break the boot if
390				-- loader hadn't been recompiled with this
391				-- function exposed.
392				if loader.command_error then
393					print(loader.command_error())
394				end
395				if not silent then
396					print("failed!")
397				end
398				if v.error ~= nil then
399					cli_execute_unparsed(v.error)
400				end
401				status = false
402			elseif v.after ~= nil then
403				pstatus = cli_execute_unparsed(v.after) == 0
404				if not pstatus and not silent then
405					print(MSG_FAILEXAF:format(v.after, k))
406				end
407				if not silent then
408					print("ok")
409				end
410				status = status and pstatus
411			end
412		end
413		::continue::
414	end
415
416	return status
417end
418
419local function readFile(name, silent)
420	local f = io.open(name)
421	if f == nil then
422		if not silent then
423			print(MSG_FAILOPENCFG:format(name))
424		end
425		return nil
426	end
427
428	local text, _ = io.read(f)
429	-- We might have read in the whole file, this won't be needed any more.
430	io.close(f)
431
432	if text == nil and not silent then
433		print(MSG_FAILREADCFG:format(name))
434	end
435	return text
436end
437
438local function checkNextboot()
439	local nextboot_file = loader.getenv("nextboot_conf")
440	local nextboot_enable = loader.getenv("nextboot_enable")
441
442	if nextboot_file == nil then
443		return
444	end
445
446	-- is nextboot_enable set in nvstore?
447	if nextboot_enable == "NO" then
448		return
449	end
450
451	local text = readFile(nextboot_file, true)
452	if text == nil then
453		return
454	end
455
456	if nextboot_enable == nil and
457	    text:match("^nextboot_enable=\"NO\"") ~= nil then
458		-- We're done; nextboot is not enabled
459		return
460	end
461
462	if not config.parse(text) then
463		print(MSG_FAILPARSECFG:format(nextboot_file))
464	end
465
466	-- Attempt to rewrite the first line and only the first line of the
467	-- nextboot_file. We overwrite it with nextboot_enable="NO", then
468	-- check for that on load.
469	-- It's worth noting that this won't work on every filesystem, so we
470	-- won't do anything notable if we have any errors in this process.
471	local nfile = io.open(nextboot_file, 'w')
472	if nfile ~= nil then
473		-- We need the trailing space here to account for the extra
474		-- character taken up by the string nextboot_enable="YES"
475		-- Or new end quotation mark lands on the S, and we want to
476		-- rewrite the entirety of the first line.
477		io.write(nfile, "nextboot_enable=\"NO\" ")
478		io.close(nfile)
479	end
480	loader.setenv("nextboot_enable", "NO")
481end
482
483-- Module exports
484config.verbose = false
485
486-- The first item in every carousel is always the default item.
487function config.getCarouselIndex(id)
488	return carousel_choices[id] or 1
489end
490
491function config.setCarouselIndex(id, idx)
492	carousel_choices[id] = idx
493end
494
495-- Returns true if we processed the file successfully, false if we did not.
496-- If 'silent' is true, being unable to read the file is not considered a
497-- failure.
498function config.processFile(name, silent)
499	if silent == nil then
500		silent = false
501	end
502
503	local text = readFile(name, silent)
504	if text == nil then
505		return silent
506	end
507
508	return config.parse(text)
509end
510
511-- silent runs will not return false if we fail to open the file
512function config.parse(text)
513	local n = 1
514	local status = true
515
516	for line in text:gmatch("([^\n]+)") do
517		if line:match("^%s*$") == nil then
518			for _, val in ipairs(pattern_table) do
519				local pattern = '^%s*' .. val.str .. '%s*(.*)';
520				local cgroups = val.groups or 2
521				local k, v, c = checkPattern(line, pattern)
522				if k ~= nil then
523					-- Offset by one, drats
524					if cgroups == 1 then
525						c = v
526						v = nil
527					end
528
529					if isValidComment(c) then
530						val.process(k, v)
531						goto nextline
532					end
533
534					break
535				end
536			end
537
538			print(MSG_MALFORMED:format(n, line))
539			status = false
540		end
541		::nextline::
542		n = n + 1
543	end
544
545	return status
546end
547
548function config.readConf(file, loaded_files)
549	if loaded_files == nil then
550		loaded_files = {}
551	end
552
553	if loaded_files[file] ~= nil then
554		return
555	end
556
557	-- We'll process loader_conf_dirs at the top-level readConf
558	local load_conf_dirs = next(loaded_files) == nil
559	print("Loading " .. file)
560
561	-- The final value of loader_conf_files is not important, so just
562	-- clobber it here.  We'll later check if it's no longer nil and process
563	-- the new value for files to read.
564	setEnv("loader_conf_files", nil)
565
566	-- These may or may not exist, and that's ok. Do a
567	-- silent parse so that we complain on parse errors but
568	-- not for them simply not existing.
569	if not config.processFile(file, true) then
570		print(MSG_FAILPARSECFG:format(file))
571	end
572
573	loaded_files[file] = true
574
575	-- Going to process "loader_conf_files" extra-files
576	local loader_conf_files = getEnv("loader_conf_files")
577	if loader_conf_files ~= nil then
578		for name in loader_conf_files:gmatch("[%w%p]+") do
579			config.readConf(name, loaded_files)
580		end
581	end
582
583	if load_conf_dirs then
584		local loader_conf_dirs = getEnv("loader_conf_dirs")
585		if loader_conf_dirs ~= nil then
586			for name in loader_conf_dirs:gmatch("[%w%p]+") do
587				if lfs.attributes(name, "mode") ~= "directory" then
588					print(MSG_FAILDIR:format(name))
589					goto nextdir
590				end
591				for cfile in lfs.dir(name) do
592					if cfile:match(".conf$") then
593						local fpath = name .. "/" .. cfile
594						if lfs.attributes(fpath, "mode") == "file" then
595							config.readConf(fpath, loaded_files)
596						end
597					end
598				end
599				::nextdir::
600			end
601		end
602	end
603end
604
605-- other_kernel is optionally the name of a kernel to load, if not the default
606-- or autoloaded default from the module_path
607function config.loadKernel(other_kernel)
608	local flags = loader.getenv("kernel_options") or ""
609	local kernel = other_kernel or loader.getenv("kernel")
610
611	local function tryLoad(names)
612		for name in names:gmatch("([^;]+)%s*;?") do
613			local r = loader.perform("load " .. name ..
614			     " " .. flags)
615			if r == 0 then
616				return name
617			end
618		end
619		return nil
620	end
621
622	local function getModulePath()
623		local module_path = loader.getenv("module_path")
624		local kernel_path = loader.getenv("kernel_path")
625
626		if kernel_path == nil then
627			return module_path
628		end
629
630		-- Strip the loaded kernel path from module_path. This currently assumes
631		-- that the kernel path will be prepended to the module_path when it's
632		-- found.
633		kernel_path = escapeName(kernel_path .. ';')
634		return module_path:gsub(kernel_path, '')
635	end
636
637	local function loadBootfile()
638		local bootfile = loader.getenv("bootfile")
639
640		-- append default kernel name
641		if bootfile == nil then
642			bootfile = "kernel"
643		else
644			bootfile = bootfile .. ";kernel"
645		end
646
647		return tryLoad(bootfile)
648	end
649
650	-- kernel not set, try load from default module_path
651	if kernel == nil then
652		local res = loadBootfile()
653
654		if res ~= nil then
655			-- Default kernel is loaded
656			config.kernel_loaded = nil
657			return true
658		else
659			print(MSG_DEFAULTKERNFAIL)
660			return false
661		end
662	else
663		-- Use our cached module_path, so we don't end up with multiple
664		-- automatically added kernel paths to our final module_path
665		local module_path = getModulePath()
666		local res
667
668		if other_kernel ~= nil then
669			kernel = other_kernel
670		end
671		-- first try load kernel with module_path = /boot/${kernel}
672		-- then try load with module_path=${kernel}
673		local paths = {"/boot/" .. kernel, kernel}
674
675		for _, v in pairs(paths) do
676			loader.setenv("module_path", v)
677			res = loadBootfile()
678
679			-- succeeded, add path to module_path
680			if res ~= nil then
681				config.kernel_loaded = kernel
682				if module_path ~= nil then
683					loader.setenv("module_path", v .. ";" ..
684					    module_path)
685					loader.setenv("kernel_path", v)
686				end
687				return true
688			end
689		end
690
691		-- failed to load with ${kernel} as a directory
692		-- try as a file
693		res = tryLoad(kernel)
694		if res ~= nil then
695			config.kernel_loaded = kernel
696			return true
697		else
698			print(MSG_KERNFAIL:format(kernel))
699			return false
700		end
701	end
702end
703
704function config.selectKernel(kernel)
705	config.kernel_selected = kernel
706end
707
708function config.load(file, reloading)
709	if not file then
710		file = "/boot/defaults/loader.conf"
711	end
712
713	config.readConf(file)
714
715	checkNextboot()
716
717	local verbose = loader.getenv("verbose_loading") or "no"
718	config.verbose = verbose:lower() == "yes"
719	if not reloading then
720		hook.runAll("config.loaded")
721	end
722end
723
724-- Reload configuration
725function config.reload(file)
726	modules = {}
727	restoreEnv()
728	config.load(file, true)
729	hook.runAll("config.reloaded")
730end
731
732function config.loadelf()
733	local xen_kernel = loader.getenv('xen_kernel')
734	local kernel = config.kernel_selected or config.kernel_loaded
735	local status
736
737	if xen_kernel ~= nil then
738		print(MSG_XENKERNLOADING)
739		if cli_execute_unparsed('load ' .. xen_kernel) ~= 0 then
740			print(MSG_XENKERNFAIL:format(xen_kernel))
741			return false
742		end
743	end
744	print(MSG_KERNLOADING)
745	if not config.loadKernel(kernel) then
746		return false
747	end
748	hook.runAll("kernel.loaded")
749
750	print(MSG_MODLOADING)
751	status = loadModule(modules, not config.verbose)
752	hook.runAll("modules.loaded")
753	return status
754end
755
756function config.enableModule(modname)
757	if modules[modname] == nil then
758		modules[modname] = {}
759	elseif modules[modname].load == "YES" then
760		modules[modname].force = true
761		return true
762	end
763
764	modules[modname].load = "YES"
765	modules[modname].force = true
766	return true
767end
768
769function config.disableModule(modname)
770	if modules[modname] == nil then
771		return false
772	elseif modules[modname].load ~= "YES" then
773		return true
774	end
775
776	modules[modname].load = "NO"
777	modules[modname].force = nil
778	return true
779end
780
781function config.isModuleEnabled(modname)
782	local mod = modules[modname]
783	if not mod or mod.load ~= "YES" then
784		return false
785	end
786
787	if mod.force then
788		return true
789	end
790
791	local blacklist = getBlacklist()
792	return not blacklist[modname]
793end
794
795function config.getModuleInfo()
796	return {
797		modules = modules,
798		blacklist = getBlacklist()
799	}
800end
801
802hook.registerType("config.loaded")
803hook.registerType("config.reloaded")
804hook.registerType("kernel.loaded")
805hook.registerType("modules.loaded")
806return config
807