forked from omriharel/deej
-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
253 lines (190 loc) · 7.54 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
package deej
import (
"fmt"
"path"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"github.com/omriharel/deej/util"
)
// CanonicalConfig provides application-wide access to configuration fields,
// as well as loading/file watching logic for deej's configuration file
type CanonicalConfig struct {
SliderMapping *sliderMap
ConnectionInfo struct {
COMPort string
BaudRate int
}
InvertSliders bool
NoiseReductionLevel string
logger *zap.SugaredLogger
notifier Notifier
stopWatcherChannel chan bool
reloadConsumers []chan bool
userConfig *viper.Viper
internalConfig *viper.Viper
}
const (
userConfigFilepath = "config.yaml"
internalConfigFilepath = "preferences.yaml"
userConfigName = "config"
internalConfigName = "preferences"
userConfigPath = "."
configType = "yaml"
configKeySliderMapping = "slider_mapping"
configKeyInvertSliders = "invert_sliders"
configKeyCOMPort = "com_port"
configKeyBaudRate = "baud_rate"
configKeyNoiseReductionLevel = "noise_reduction"
defaultCOMPort = "COM4"
defaultBaudRate = 9600
)
// has to be defined as a non-constant because we're using path.Join
var internalConfigPath = path.Join(".", logDirectory)
var defaultSliderMapping = func() *sliderMap {
emptyMap := newSliderMap()
emptyMap.set(0, []string{masterSessionName})
return emptyMap
}()
// NewConfig creates a config instance for the deej object and sets up viper instances for deej's config files
func NewConfig(logger *zap.SugaredLogger, notifier Notifier) (*CanonicalConfig, error) {
logger = logger.Named("config")
cc := &CanonicalConfig{
logger: logger,
notifier: notifier,
reloadConsumers: []chan bool{},
stopWatcherChannel: make(chan bool),
}
// distinguish between the user-provided config (config.yaml) and the internal config (logs/preferences.yaml)
userConfig := viper.New()
userConfig.SetConfigName(userConfigName)
userConfig.SetConfigType(configType)
userConfig.AddConfigPath(userConfigPath)
userConfig.SetDefault(configKeySliderMapping, map[string][]string{})
userConfig.SetDefault(configKeyInvertSliders, false)
userConfig.SetDefault(configKeyCOMPort, defaultCOMPort)
userConfig.SetDefault(configKeyBaudRate, defaultBaudRate)
internalConfig := viper.New()
internalConfig.SetConfigName(internalConfigName)
internalConfig.SetConfigType(configType)
internalConfig.AddConfigPath(internalConfigPath)
cc.userConfig = userConfig
cc.internalConfig = internalConfig
logger.Debug("Created config instance")
return cc, nil
}
// Load reads deej's config files from disk and tries to parse them
func (cc *CanonicalConfig) Load() error {
cc.logger.Debugw("Loading config", "path", userConfigFilepath)
// make sure it exists
if !util.FileExists(userConfigFilepath) {
cc.logger.Warnw("Config file not found", "path", userConfigFilepath)
cc.notifier.Notify("Can't find configuration!",
fmt.Sprintf("%s must be in the same directory as deej. Please re-launch", userConfigFilepath))
return fmt.Errorf("config file doesn't exist: %s", userConfigFilepath)
}
// load the user config
if err := cc.userConfig.ReadInConfig(); err != nil {
cc.logger.Warnw("Viper failed to read user config", "error", err)
// if the error is yaml-format-related, show a sensible error. otherwise, show 'em to the logs
if strings.Contains(err.Error(), "yaml:") {
cc.notifier.Notify("Invalid configuration!",
fmt.Sprintf("Please make sure %s is in a valid YAML format.", userConfigFilepath))
} else {
cc.notifier.Notify("Error loading configuration!", "Please check deej's logs for more details.")
}
return fmt.Errorf("read user config: %w", err)
}
// load the internal config - this doesn't have to exist, so it can error
if err := cc.internalConfig.ReadInConfig(); err != nil {
cc.logger.Debugw("Viper failed to read internal config", "error", err, "reminder", "this is fine")
}
// canonize the configuration with viper's helpers
if err := cc.populateFromVipers(); err != nil {
cc.logger.Warnw("Failed to populate config fields", "error", err)
return fmt.Errorf("populate config fields: %w", err)
}
cc.logger.Info("Loaded config successfully")
cc.logger.Infow("Config values",
"sliderMapping", cc.SliderMapping,
"connectionInfo", cc.ConnectionInfo,
"invertSliders", cc.InvertSliders)
return nil
}
// SubscribeToChanges allows external components to receive updates when the config is reloaded
func (cc *CanonicalConfig) SubscribeToChanges() chan bool {
c := make(chan bool)
cc.reloadConsumers = append(cc.reloadConsumers, c)
return c
}
// WatchConfigFileChanges starts watching for configuration file changes
// and attempts reloading the config when they happen
func (cc *CanonicalConfig) WatchConfigFileChanges() {
cc.logger.Debugw("Starting to watch user config file for changes", "path", userConfigFilepath)
const (
minTimeBetweenReloadAttempts = time.Millisecond * 500
delayBetweenEventAndReload = time.Millisecond * 50
)
lastAttemptedReload := time.Now()
// establish watch using viper as opposed to doing it ourselves, though our internal cooldown is still required
cc.userConfig.WatchConfig()
cc.userConfig.OnConfigChange(func(event fsnotify.Event) {
// when we get a write event...
if event.Op&fsnotify.Write == fsnotify.Write {
now := time.Now()
// ... check if it's not a duplicate (many editors will write to a file twice)
if lastAttemptedReload.Add(minTimeBetweenReloadAttempts).Before(now) {
// and attempt reload if appropriate
cc.logger.Debugw("Config file modified, attempting reload", "event", event)
// wait a bit to let the editor actually flush the new file contents to disk
<-time.After(delayBetweenEventAndReload)
if err := cc.Load(); err != nil {
cc.logger.Warnw("Failed to reload config file", "error", err)
} else {
cc.logger.Info("Reloaded config successfully")
cc.notifier.Notify("Configuration reloaded!", "Your changes have been applied.")
cc.onConfigReloaded()
}
// don't forget to update the time
lastAttemptedReload = now
}
}
})
// wait till they stop us
<-cc.stopWatcherChannel
cc.logger.Debug("Stopping user config file watcher")
cc.userConfig.OnConfigChange(nil)
}
// StopWatchingConfigFile signals our filesystem watcher to stop
func (cc *CanonicalConfig) StopWatchingConfigFile() {
cc.stopWatcherChannel <- true
}
func (cc *CanonicalConfig) populateFromVipers() error {
// merge the slider mappings from the user and internal configs
cc.SliderMapping = sliderMapFromConfigs(
cc.userConfig.GetStringMapStringSlice(configKeySliderMapping),
cc.internalConfig.GetStringMapStringSlice(configKeySliderMapping),
)
// get the rest of the config fields - viper saves us a lot of effort here
cc.ConnectionInfo.COMPort = cc.userConfig.GetString(configKeyCOMPort)
cc.ConnectionInfo.BaudRate = cc.userConfig.GetInt(configKeyBaudRate)
if cc.ConnectionInfo.BaudRate <= 0 {
cc.logger.Warnw("Invalid baud rate specified, using default value",
"key", configKeyBaudRate,
"invalidValue", cc.ConnectionInfo.BaudRate,
"defaultValue", defaultBaudRate)
cc.ConnectionInfo.BaudRate = defaultBaudRate
}
cc.InvertSliders = cc.userConfig.GetBool(configKeyInvertSliders)
cc.NoiseReductionLevel = cc.userConfig.GetString(configKeyNoiseReductionLevel)
cc.logger.Debug("Populated config fields from vipers")
return nil
}
func (cc *CanonicalConfig) onConfigReloaded() {
cc.logger.Debug("Notifying consumers about configuration reload")
for _, consumer := range cc.reloadConsumers {
consumer <- true
}
}