Let the operator edit the station config in the window instead of hand-editing the file and relaunching.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+126
-40
@@ -51,80 +51,162 @@ type templateFile struct {
|
||||
|
||||
var callsignPattern = regexp.MustCompile(`^[A-Z0-9][A-Z]{0,2}\d[A-Z0-9]{0,3}[A-Z](?:-[A-Z0-9]{1,3})?$`)
|
||||
|
||||
func loadConfig(path string) (loaded, error) {
|
||||
// configLoad is the result of reading a config path. err from loadConfig is
|
||||
// only a failure that still exits before the window, such as an unknown home
|
||||
// directory. A missing or invalid file is reported in detail with loaded false.
|
||||
type configLoad struct {
|
||||
cfg loaded
|
||||
form ConfigForm
|
||||
loaded bool
|
||||
detail string
|
||||
}
|
||||
|
||||
func loadConfig(path string) (configLoad, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := writeTemplate(path); err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
return loaded{}, fmt.Errorf("created %s", path)
|
||||
form, ferr := templateForm()
|
||||
if ferr != nil {
|
||||
return configLoad{}, ferr
|
||||
}
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return configLoad{form: form}, nil
|
||||
}
|
||||
return configLoad{form: form, detail: fmt.Errorf("the file could not be read: %w", err).Error()}, nil
|
||||
}
|
||||
return parseConfig(path, body)
|
||||
}
|
||||
|
||||
func writeTemplate(path string) error {
|
||||
logFile, err := defaultLogPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := jsonv2.Marshal(templateFile{CertificateDir: "./funknetz", LogFile: logFile})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = append(body, '\n')
|
||||
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 {
|
||||
return werr
|
||||
}
|
||||
if cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
return os.Chmod(path, 0o600)
|
||||
// ConfigForm is the eight fields the Config window edits.
|
||||
type ConfigForm struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Callsign string `json:"callsign"`
|
||||
AuthKey string `json:"authKey"`
|
||||
TalkGroup int `json:"talkGroup"`
|
||||
CertificateDir string `json:"certificateDir"`
|
||||
EnableLogging bool `json:"enableLogging"`
|
||||
LogFile string `json:"logFile"`
|
||||
}
|
||||
|
||||
func parseConfig(path string, body []byte) (loaded, error) {
|
||||
// ConfigState is what the Config window shows.
|
||||
type ConfigState struct {
|
||||
Open bool `json:"open"`
|
||||
NeedsConfig bool `json:"needsConfig"`
|
||||
Error string `json:"error"`
|
||||
Form ConfigForm `json:"form"`
|
||||
}
|
||||
|
||||
func templateForm() (ConfigForm, error) {
|
||||
logFile, err := defaultLogPath()
|
||||
if err != nil {
|
||||
return ConfigForm{}, err
|
||||
}
|
||||
return ConfigForm{CertificateDir: "./funknetz", LogFile: logFile}, nil
|
||||
}
|
||||
|
||||
func parseConfig(path string, body []byte) (configLoad, error) {
|
||||
var shape fileShape
|
||||
if err := jsonv2.Unmarshal(body, &shape); err != nil {
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
form, ferr := templateForm()
|
||||
if ferr != nil {
|
||||
return configLoad{}, ferr
|
||||
}
|
||||
return configLoad{form: form, detail: fmt.Errorf("the file could not be read: %w", err).Error()}, nil
|
||||
}
|
||||
certBad := present(shape.CertificateDir) && shape.CertificateDir.Kind() != jsontext.KindString
|
||||
if block, notice := blockedLog(shape); block || certBad {
|
||||
cfg, err := parseOperational(shape)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
return refused(shape, err)
|
||||
}
|
||||
cfg.logNotice = notice
|
||||
if !block {
|
||||
cfg = applyLog(cfg, shape)
|
||||
}
|
||||
return applyCertificate(cfg, shape, path), nil
|
||||
return finish(shape, applyCertificate(cfg, shape, path))
|
||||
}
|
||||
if missingCertificate(shape) || missingLog(shape) {
|
||||
if !operationalKindsOK(shape) {
|
||||
return parseOperational(shape)
|
||||
_, err := parseOperational(shape)
|
||||
if err != nil {
|
||||
return refused(shape, err)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
shape, err = withLogDefaults(shape)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
return configLoad{}, err
|
||||
}
|
||||
if err := rewriteConfig(path, shape); err != nil {
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
return configLoad{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
}
|
||||
}
|
||||
cfg, err := parseOperational(shape)
|
||||
if err != nil {
|
||||
return loaded{}, err
|
||||
return refused(shape, err)
|
||||
}
|
||||
return applyLog(applyCertificate(cfg, shape, path), shape), nil
|
||||
return finish(shape, applyLog(applyCertificate(cfg, shape, path), shape))
|
||||
}
|
||||
|
||||
func finish(shape fileShape, cfg loaded) (configLoad, error) {
|
||||
form, err := formFromShape(shape)
|
||||
if err != nil {
|
||||
return configLoad{}, err
|
||||
}
|
||||
if cfg.certFatal != "" {
|
||||
return configLoad{cfg: cfg, form: form, detail: cfg.certFatal}, nil
|
||||
}
|
||||
return configLoad{cfg: cfg, form: form, loaded: true, detail: cfg.logNotice}, nil
|
||||
}
|
||||
|
||||
func refused(shape fileShape, cause error) (configLoad, error) {
|
||||
form, err := formFromShape(shape)
|
||||
if err != nil {
|
||||
return configLoad{}, err
|
||||
}
|
||||
return configLoad{form: form, detail: cause.Error()}, nil
|
||||
}
|
||||
|
||||
func formFromShape(shape fileShape) (ConfigForm, error) {
|
||||
fallback, err := templateForm()
|
||||
if err != nil {
|
||||
fallback = ConfigForm{CertificateDir: "./funknetz"}
|
||||
}
|
||||
return mergeSalvage(shape, fallback), nil
|
||||
}
|
||||
|
||||
func mergeSalvage(shape fileShape, fallback ConfigForm) ConfigForm {
|
||||
if s, err := stringField("host", shape.Host); err == nil && present(shape.Host) {
|
||||
fallback.Host = s
|
||||
}
|
||||
if present(shape.Port) && shape.Port.Kind() == jsontext.KindNumber {
|
||||
var n int
|
||||
if jsonv2.Unmarshal(shape.Port, &n) == nil {
|
||||
fallback.Port = n
|
||||
}
|
||||
}
|
||||
if s, err := stringField("callsign", shape.Callsign); err == nil && present(shape.Callsign) {
|
||||
fallback.Callsign = s
|
||||
}
|
||||
if s, err := stringField("authKey", shape.AuthKey); err == nil && present(shape.AuthKey) {
|
||||
fallback.AuthKey = s
|
||||
}
|
||||
if present(shape.TalkGroup) && shape.TalkGroup.Kind() == jsontext.KindNumber {
|
||||
var n int
|
||||
if jsonv2.Unmarshal(shape.TalkGroup, &n) == nil {
|
||||
fallback.TalkGroup = n
|
||||
}
|
||||
}
|
||||
if s, err := stringField("certificateDir", shape.CertificateDir); err == nil && present(shape.CertificateDir) {
|
||||
fallback.CertificateDir = s
|
||||
}
|
||||
if present(shape.EnableLogging) && isBool(shape.EnableLogging) {
|
||||
fallback.EnableLogging = shape.EnableLogging.Kind() == jsontext.KindTrue
|
||||
}
|
||||
if s, err := stringField("logFile", shape.LogFile); err == nil && present(shape.LogFile) {
|
||||
fallback.LogFile = s
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func parseOperational(shape fileShape) (loaded, error) {
|
||||
@@ -304,13 +386,17 @@ func rewriteConfig(path string, shape fileShape) error {
|
||||
return err
|
||||
}
|
||||
body = append(body, '\n')
|
||||
return rewriteBytes(path, body, info.Mode().Perm())
|
||||
}
|
||||
|
||||
func rewriteBytes(path string, body []byte, mode fs.FileMode) error {
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), ".funknetz-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
_, werr := tmp.Write(body)
|
||||
cerr := tmp.Chmod(info.Mode().Perm())
|
||||
cerr := tmp.Chmod(mode)
|
||||
clerr := tmp.Close()
|
||||
if werr != nil || cerr != nil || clerr != nil {
|
||||
os.Remove(tmpName)
|
||||
|
||||
Reference in New Issue
Block a user