1226
|
1 const CONFIGROOT: string = "."
|
|
2 const CONFIGEXTENSION: string = "ini"
|
|
3 # by default enable hot-reload of runtime-configuration only in debug builds
|
|
4 const CONFIGHOTRELOAD: bool = not defined(release)
|
|
5 # milliseconds to wait between checks for settings hotreload
|
|
6 const CONFIGHOTRELOADINTERVAL: int = 1000
|
|
7
|
|
8 when CONFIGHOTRELOAD:
|
|
9 var
|
|
10 configUpdates: Channel[(string, string)]
|
|
11 configUpdates.open()
|
|
12
|
|
13 # runtime configuration
|
|
14 # =====================
|
|
15 # namespace is the path from the CONFIGROOT to the according settings file without the file extension
|
|
16 # a settings file must always have the extension CONFIGEXTENSION
|
|
17 # a fully qualified settings identifier can be in the form {namespace}.{section}.{key}
|
|
18 # {key} and {section} may not contain dots
|
|
19
|
|
20 # a "namespace" is the path from the settings root to an *.CONFIGEXTENSION file, without the file extension
|
|
21 # settings is a namespace <-> settings mapping
|
|
22 var allsettings: Table[string, Config]
|
|
23
|
|
24 proc configRoot(): string =
|
|
25 joinPath(absolutePath(getAppDir()), CONFIGROOT)
|
|
26
|
|
27 proc getFile(namespace: string): string =
|
|
28 joinPath(configRoot(), namespace & "." & CONFIGEXTENSION)
|
|
29
|
|
30 iterator walkConfigNamespaces(): string =
|
|
31 for file in walkDirRec(dir = configRoot(), relative = true, checkDir = true):
|
|
32 if file.endsWith("." & CONFIGEXTENSION):
|
|
33 yield file[0 ..< ^(CONFIGEXTENSION.len + 1)]
|
|
34
|
|
35 proc loadAllConfig(): Table[string, Config] =
|
|
36 for ns in walkConfigNamespaces():
|
|
37 result[ns] = ns.getFile().loadConfig()
|
|
38
|
|
39 proc ReloadSettings*() =
|
|
40 allsettings = loadAllConfig()
|
|
41
|
|
42 proc configStr(key, section, namespace: string): string =
|
|
43 when CONFIGHOTRELOAD:
|
|
44 while configUpdates.peek() > 0:
|
|
45 let (updatedNamespace, updatedConfig) = configUpdates.recv()
|
|
46 allsettings[updatedNamespace] = loadConfig(newStringStream(updatedConfig))
|
|
47 if not allsettings.hasKey(namespace):
|
|
48 raise newException(Exception, &"Settings {namespace}.{section}.{key} was not found")
|
|
49 allsettings[namespace].getSectionValue(section, key)
|
|
50
|
|
51 proc Setting*[T: int|float|string](key, section, namespace: string): T =
|
|
52 when T is int:
|
|
53 let value = configStr(key, section, namespace)
|
|
54 if parseInt(value, result) == 0:
|
|
55 raise newException(Exception, &"Unable to parse int from settings {namespace}.{section}.{key}: {value}")
|
|
56 elif T is float:
|
|
57 let value = configStr(key, section, namespace)
|
|
58 if parseFloat(value, result) == 0:
|
|
59 raise newException(Exception, &"Unable to parse float from settings {namespace}.{section}.{key}: {value}")
|
|
60 else:
|
|
61 result = configStr(key, section, namespace)
|
|
62
|
|
63 proc Setting*[T: int|float|string](identifier: string): T =
|
|
64 # identifier can be in the form:
|
|
65 # {namespace}.{key}
|
|
66 # {namespace}.{section}.{key}
|
|
67 let parts = identifier.rsplit(".")
|
|
68 if parts.len == 1:
|
|
69 raise newException(Exception, &"Setting with name {identifier} has no namespace")
|
|
70 if parts.len == 2: result = Setting[T](parts[1], "", parts[0])
|
|
71 else: result = Setting[T](parts[^1], parts[^2], joinPath(parts[0 .. ^3]))
|
|
72
|
|
73 proc HadConfigUpdate*(): bool =
|
|
74 when CONFIGHOTRELOAD == true:
|
|
75 result = configUpdates.peek() > 0
|
|
76
|
|
77 allsettings = loadAllConfig()
|
|
78
|
|
79 when CONFIGHOTRELOAD == true:
|
|
80 import std/times
|
|
81
|
|
82 proc configFileWatchdog() {.thread.} =
|
|
83 var configModTimes: Table[string, times.Time]
|
|
84 while true:
|
|
85 for namespace in walkConfigNamespaces():
|
|
86 if not (namespace in configModTimes):
|
|
87 configModTimes[namespace] = times.Time()
|
|
88 let lastMod = namespace.getFile().getLastModificationTime()
|
|
89 if lastMod > configModTimes[namespace]:
|
|
90 configModTimes[namespace] = lastMod
|
|
91 let configStr = newFileStream(namespace.getFile()).readAll()
|
|
92 configUpdates.send((namespace, configStr))
|
|
93 sleep CONFIGHOTRELOADINTERVAL
|
|
94 var thethread: Thread[void]
|
|
95 createThread(thethread, configFileWatchdog)
|
|
96
|
|
97 if not defined(release):
|
|
98 setLogFilter(lvlAll)
|
|
99 else:
|
|
100 setLogFilter(lvlWarn)
|