297 lines
6.4 KiB
Go
297 lines
6.4 KiB
Go
package station
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"strings"
|
|
|
|
"encoding/json/jsontext"
|
|
jsonv2 "encoding/json/v2"
|
|
)
|
|
|
|
// Cancel discards the Config window. It reports whether the app should quit
|
|
// because no config has loaded successfully.
|
|
func (s *Station) Cancel() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if !s.loaded {
|
|
return true
|
|
}
|
|
s.configOpen = false
|
|
s.configErr = ""
|
|
s.publish()
|
|
return false
|
|
}
|
|
|
|
// OpenConfig re-reads the config file into the form. The live session is kept
|
|
// when the re-read fails.
|
|
func (s *Station) OpenConfig() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.configOpen {
|
|
return
|
|
}
|
|
if s.buttonHeld || s.spaceHeld {
|
|
s.openBlocked = "Release push-to-talk"
|
|
s.publish()
|
|
return
|
|
}
|
|
s.openBlocked = ""
|
|
form, detail, ok := readForm(s.path)
|
|
s.configOpen = true
|
|
s.form = form
|
|
if ok {
|
|
s.configErr = ""
|
|
} else {
|
|
s.configErr = detail
|
|
}
|
|
s.publish()
|
|
}
|
|
|
|
// Save validates form, writes the config file, and closes the Config window.
|
|
// An invalid form or a write error leaves the window open and the session unchanged.
|
|
func (s *Station) Save(form ConfigForm) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
next, stored, err := validateForm(s.path, form)
|
|
if err != nil {
|
|
s.keepForm(form, err.Error())
|
|
return err
|
|
}
|
|
if err := writeForm(s.path, stored); err != nil {
|
|
msg := fmt.Errorf("the file could not be written: %w", err).Error()
|
|
s.keepForm(form, msg)
|
|
return errors.New(msg)
|
|
}
|
|
prev := s.cfg
|
|
wasLoaded := s.loaded
|
|
s.cfg = next
|
|
s.form = stored
|
|
s.loaded = true
|
|
s.configErr = ""
|
|
s.configOpen = false
|
|
s.logPath = next.logPath
|
|
s.logNotice = next.logNotice
|
|
reconnect := wasLoaded && connectionChanged(prev, next)
|
|
if next.certFatal != "" {
|
|
s.fatal = next.certFatal
|
|
s.closeLogLocked()
|
|
if next.enableLogging {
|
|
s.openLog()
|
|
}
|
|
if wasLoaded {
|
|
s.kickReconnect()
|
|
}
|
|
} else if reconnect || !wasLoaded {
|
|
if !wasLoaded || s.fatal == prev.certFatal {
|
|
s.fatal = ""
|
|
}
|
|
s.closeLogLocked()
|
|
if next.enableLogging {
|
|
s.openLog()
|
|
}
|
|
if reconnect {
|
|
s.kickReconnect()
|
|
}
|
|
} else {
|
|
s.applyLogging(prev, next)
|
|
}
|
|
s.publish()
|
|
return nil
|
|
}
|
|
|
|
func (s *Station) kickReconnect() {
|
|
s.epoch++
|
|
s.kick = true
|
|
if s.cancelDial != nil {
|
|
s.cancelDial()
|
|
}
|
|
if s.conn != nil {
|
|
// Close unblocks the session read so Run can dial again.
|
|
_ = s.conn.Close()
|
|
}
|
|
select {
|
|
case s.apply <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (s *Station) drainApply() {
|
|
select {
|
|
case <-s.apply:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (s *Station) applyLogging(prev, next loaded) {
|
|
if prev.enableLogging == next.enableLogging && prev.logPath == next.logPath && prev.logNotice == next.logNotice {
|
|
return
|
|
}
|
|
s.closeLogLocked()
|
|
s.logNotice = next.logNotice
|
|
s.logPath = next.logPath
|
|
if next.enableLogging && next.logNotice == "" {
|
|
s.openLog()
|
|
}
|
|
}
|
|
|
|
func connectionChanged(prev, next loaded) bool {
|
|
return prev.host != next.host || prev.port != next.port || prev.callsign != next.callsign ||
|
|
prev.authKey != next.authKey || prev.talkGroup != next.talkGroup || prev.certificateDir != next.certificateDir
|
|
}
|
|
|
|
func readForm(path string) (ConfigForm, string, bool) {
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
form, ferr := templateForm()
|
|
if ferr != nil {
|
|
form = ConfigForm{CertificateDir: "./funknetz"}
|
|
}
|
|
return form, fmt.Errorf("the file could not be read: %w", err).Error(), false
|
|
}
|
|
out, err := parseConfig(path, body, false)
|
|
if err != nil {
|
|
return out.form, err.Error(), false
|
|
}
|
|
if !out.loaded {
|
|
msg := out.detail
|
|
if msg == "" {
|
|
msg = "the file could not be read"
|
|
}
|
|
return out.form, msg, false
|
|
}
|
|
return out.form, "", true
|
|
}
|
|
|
|
func (s *Station) keepForm(form ConfigForm, msg string) {
|
|
s.form = form
|
|
s.configErr = msg
|
|
s.configOpen = true
|
|
s.publish()
|
|
}
|
|
|
|
func validateForm(configPath string, form ConfigForm) (loaded, ConfigForm, error) {
|
|
shape, err := shapeFromForm(form)
|
|
if err != nil {
|
|
return loaded{}, form, err
|
|
}
|
|
cfg, err := parseOperational(shape)
|
|
if err != nil {
|
|
return loaded{}, form, err
|
|
}
|
|
cfg = applyCertificate(cfg, shape, configPath)
|
|
cfg = applyLog(cfg, shape)
|
|
stored := ConfigForm{
|
|
Host: cfg.host,
|
|
Port: cfg.port,
|
|
Callsign: cfg.callsign,
|
|
AuthKey: cfg.authKey,
|
|
TalkGroup: cfg.talkGroup,
|
|
CertificateDir: strings.TrimSpace(form.CertificateDir),
|
|
EnableLogging: form.EnableLogging,
|
|
LogFile: form.LogFile,
|
|
}
|
|
return cfg, stored, nil
|
|
}
|
|
|
|
func shapeFromForm(form ConfigForm) (fileShape, error) {
|
|
host, err := jsonValue(form.Host)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
port, err := jsonValue(form.Port)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
call, err := jsonValue(form.Callsign)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
key, err := jsonValue(form.AuthKey)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
tg, err := jsonValue(form.TalkGroup)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
dir, err := jsonValue(form.CertificateDir)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
en, err := jsonValue(form.EnableLogging)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
logFile, err := jsonValue(form.LogFile)
|
|
if err != nil {
|
|
return fileShape{}, err
|
|
}
|
|
return fileShape{
|
|
Host: host,
|
|
Port: port,
|
|
Callsign: call,
|
|
AuthKey: key,
|
|
TalkGroup: tg,
|
|
CertificateDir: dir,
|
|
EnableLogging: en,
|
|
LogFile: logFile,
|
|
}, nil
|
|
}
|
|
|
|
func jsonValue(v any) (jsontext.Value, error) {
|
|
raw, err := jsonv2.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return jsontext.Value(raw), nil
|
|
}
|
|
|
|
func writeForm(path string, form ConfigForm) error {
|
|
body, err := jsonv2.Marshal(templateFile{
|
|
Host: form.Host,
|
|
Port: form.Port,
|
|
Callsign: form.Callsign,
|
|
AuthKey: form.AuthKey,
|
|
TalkGroup: form.TalkGroup,
|
|
CertificateDir: form.CertificateDir,
|
|
EnableLogging: form.EnableLogging,
|
|
LogFile: form.LogFile,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = append(body, '\n')
|
|
info, err := os.Stat(path)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return createExclusive(path, body)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return rewriteBytes(path, body, info.Mode().Perm())
|
|
}
|
|
|
|
func createExclusive(path string, body []byte) error {
|
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, werr := f.Write(body)
|
|
cerr := f.Close()
|
|
if werr != nil || cerr != nil {
|
|
os.Remove(path)
|
|
if werr != nil {
|
|
return werr
|
|
}
|
|
return cerr
|
|
}
|
|
if err := os.Chmod(path, 0o600); err != nil {
|
|
os.Remove(path)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|