report configuration failures instead of crashing

Return errors from the save path rather than panicking.
Every write panicked on failure, so a read-only or missing
configuration directory killed a running client. The parent directory
is created when absent, and callers now show the failure in the output
window and carry on.

Write through an unpredictable temporary file.
The old fixed ".tmp" name next to the configuration was a symlink
target an attacker could plant in advance.

Serialize configuration reads and writes.
Hotkey handlers, the audio thread, and the connection callbacks all
touch the same structure, so saves could interleave with updates.

Parse addresses with SplitHostPort.
Splitting on every colon broke IPv6 addresses and panicked outright on
an address with no port. Both now fall back to Mumble's default port.

Fail immediately when an explicitly requested config file is missing
or is not a regular file.
Silently falling back to defaults hid a mistyped -config path.

Supply defaults for the clear-output and scroll-to-top and -bottom
hotkeys.
The UI registered listeners for them but the configuration never
filled the keys in, so the bindings were nil and the keys did nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-20 14:42:52 -04:00
co-authored by Claude Opus 5
parent 7e1faba06b
commit 17173b779b
7 changed files with 235 additions and 32 deletions
+18 -5
View File
@@ -114,6 +114,8 @@ func main() {
fifo := flag.String("fifo", "", "path of a FIFO from which to read commands")
serverSet := false
usernameSet := false
configSet := false
certificateSet := false
buffers := flag.Int("buffers", 16, "number of audio buffers to use")
audioInterval := flag.Int("audio-interval", 10, "outgoing audio packet duration in ms (10, 20, 40, or 60)")
jitterBuffer := flag.Int("jitter-buffer", 40, "incoming per-user audio buffer in ms (0, 20, 40, or 60)")
@@ -165,19 +167,24 @@ func main() {
}()
}
userConfig := config.NewConfig(cfgfn)
certificateSet := false
flag.CommandLine.Visit(func(theFlag *flag.Flag) {
switch theFlag.Name {
case "server":
serverSet = true
case "username":
usernameSet = true
case "config":
configSet = true
case "certificate":
certificateSet = true
}
})
if configSet {
if err := config.RequireConfigFile(*cfgfn); err != nil {
handle_raw_error(err)
}
}
userConfig := config.NewConfig(cfgfn)
if !serverSet {
server = userConfig.GetDefaultServer()
@@ -229,13 +236,19 @@ func main() {
b.Config.IncomingAudioBuffer = selectedJitterBuffer
b.Hotkeys = b.UserConfig.GetHotkeys()
b.UserConfig.SaveConfig()
if err := b.UserConfig.SaveConfig(); err != nil {
fmt.Fprintf(os.Stderr, "could not save configuration: %s\n", err)
os.Exit(1)
}
// Configure noise suppression
enabled := b.UserConfig.GetNoiseSuppressionEnabled()
if *noiseSuppressionEnabled {
enabled = true
b.UserConfig.SetNoiseSuppressionEnabled(true)
if err := b.UserConfig.SetNoiseSuppressionEnabled(true); err != nil {
fmt.Fprintf(os.Stderr, "could not save configuration: %s\n", err)
os.Exit(1)
}
}
b.NoiseSuppressor.SetEnabled(enabled)