Keep certificate-directory problems on the station window and leave a hand-edit untouched until Save.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
"gitea.neitzel.de/konrad/funknetz/audio"
|
"gitea.neitzel.de/konrad/funknetz/audio"
|
||||||
"gitea.neitzel.de/konrad/funknetz/station"
|
"gitea.neitzel.de/konrad/funknetz/station"
|
||||||
@@ -67,7 +67,7 @@ func (a *App) CancelConfig() {
|
|||||||
}
|
}
|
||||||
quit := a.quit
|
quit := a.quit
|
||||||
if quit == nil {
|
if quit == nil {
|
||||||
quit = wailsruntime.Quit
|
quit = runtime.Quit
|
||||||
}
|
}
|
||||||
if a.ctx != nil {
|
if a.ctx != nil {
|
||||||
quit(a.ctx)
|
quit(a.ctx)
|
||||||
|
|||||||
Vendored
+5
-2
@@ -67,10 +67,13 @@ main {
|
|||||||
|
|
||||||
#config {
|
#config {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
top: 64px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(16, 20, 24, 0.82);
|
background: rgba(16, 20, 24, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
#config[hidden] {
|
#config[hidden] {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -42,7 +42,7 @@ function growWindow() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
savedSize = size;
|
savedSize = size;
|
||||||
rt.WindowSetSize(Math.max(size.w, 420), configHeight);
|
rt.WindowSetSize(Math.max(size.w, 420), Math.max(size.h, configHeight));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-16
@@ -73,7 +73,7 @@ func loadConfig(path string) (configLoad, error) {
|
|||||||
}
|
}
|
||||||
return configLoad{form: form, detail: fmt.Errorf("the file could not be read: %w", err).Error()}, nil
|
return configLoad{form: form, detail: fmt.Errorf("the file could not be read: %w", err).Error()}, nil
|
||||||
}
|
}
|
||||||
return parseConfig(path, body)
|
return parseConfig(path, body, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigForm is the eight fields the Config window edits.
|
// ConfigForm is the eight fields the Config window edits.
|
||||||
@@ -104,7 +104,7 @@ func templateForm() (ConfigForm, error) {
|
|||||||
return ConfigForm{CertificateDir: "./funknetz", LogFile: logFile}, nil
|
return ConfigForm{CertificateDir: "./funknetz", LogFile: logFile}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseConfig(path string, body []byte) (configLoad, error) {
|
func parseConfig(path string, body []byte, rewrite bool) (configLoad, error) {
|
||||||
var shape fileShape
|
var shape fileShape
|
||||||
if err := jsonv2.Unmarshal(body, &shape); err != nil {
|
if err := jsonv2.Unmarshal(body, &shape); err != nil {
|
||||||
form, ferr := templateForm()
|
form, ferr := templateForm()
|
||||||
@@ -137,8 +137,10 @@ func parseConfig(path string, body []byte) (configLoad, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return configLoad{}, err
|
return configLoad{}, err
|
||||||
}
|
}
|
||||||
if err := rewriteConfig(path, shape); err != nil {
|
if rewrite {
|
||||||
return configLoad{}, fmt.Errorf("the file could not be read: %w", err)
|
if err := rewriteConfig(path, shape); err != nil {
|
||||||
|
return configLoad{}, fmt.Errorf("the file could not be read: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cfg, err := parseOperational(shape)
|
cfg, err := parseOperational(shape)
|
||||||
@@ -149,30 +151,23 @@ func parseConfig(path string, body []byte) (configLoad, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func finish(shape fileShape, cfg loaded) (configLoad, error) {
|
func finish(shape fileShape, cfg loaded) (configLoad, error) {
|
||||||
form, err := formFromShape(shape)
|
form := formFromShape(shape)
|
||||||
if err != nil {
|
|
||||||
return configLoad{}, err
|
|
||||||
}
|
|
||||||
if cfg.certFatal != "" {
|
if cfg.certFatal != "" {
|
||||||
return configLoad{cfg: cfg, form: form, detail: cfg.certFatal}, nil
|
return configLoad{cfg: cfg, form: form, loaded: true, detail: cfg.certFatal}, nil
|
||||||
}
|
}
|
||||||
return configLoad{cfg: cfg, form: form, loaded: true, detail: cfg.logNotice}, nil
|
return configLoad{cfg: cfg, form: form, loaded: true, detail: cfg.logNotice}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func refused(shape fileShape, cause error) (configLoad, error) {
|
func refused(shape fileShape, cause error) (configLoad, error) {
|
||||||
form, err := formFromShape(shape)
|
return configLoad{form: formFromShape(shape), detail: cause.Error()}, nil
|
||||||
if err != nil {
|
|
||||||
return configLoad{}, err
|
|
||||||
}
|
|
||||||
return configLoad{form: form, detail: cause.Error()}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formFromShape(shape fileShape) (ConfigForm, error) {
|
func formFromShape(shape fileShape) ConfigForm {
|
||||||
fallback, err := templateForm()
|
fallback, err := templateForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fallback = ConfigForm{CertificateDir: "./funknetz"}
|
fallback = ConfigForm{CertificateDir: "./funknetz"}
|
||||||
}
|
}
|
||||||
return mergeSalvage(shape, fallback), nil
|
return mergeSalvage(shape, fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeSalvage(shape fileShape, fallback ConfigForm) ConfigForm {
|
func mergeSalvage(shape fileShape, fallback ConfigForm) ConfigForm {
|
||||||
|
|||||||
@@ -446,7 +446,10 @@ func TestInvalidSaveWritesNothingAndDoesNotConnect(t *testing.T) {
|
|||||||
t.Cleanup(st.Close)
|
t.Cleanup(st.Close)
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go func() { _ = st.Run(ctx) }()
|
go func() {
|
||||||
|
// Run's error is observed through State, which is the station seam.
|
||||||
|
_ = st.Run(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
err = st.Save(station.ConfigForm{
|
err = st.Save(station.ConfigForm{
|
||||||
Host: "",
|
Host: "",
|
||||||
@@ -496,7 +499,10 @@ func TestValidSaveCreatesFileAndConnects(t *testing.T) {
|
|||||||
t.Cleanup(st.Close)
|
t.Cleanup(st.Close)
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go func() { _ = st.Run(ctx) }()
|
go func() {
|
||||||
|
// Run's error is observed through State, which is the station seam.
|
||||||
|
_ = st.Run(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
err = st.Save(station.ConfigForm{
|
err = st.Save(station.ConfigForm{
|
||||||
Host: " 127.0.0.1 ",
|
Host: " 127.0.0.1 ",
|
||||||
|
|||||||
+26
-12
@@ -75,8 +75,19 @@ func (s *Station) Save(form ConfigForm) error {
|
|||||||
s.logPath = next.logPath
|
s.logPath = next.logPath
|
||||||
s.logNotice = next.logNotice
|
s.logNotice = next.logNotice
|
||||||
reconnect := wasLoaded && connectionChanged(prev, next)
|
reconnect := wasLoaded && connectionChanged(prev, next)
|
||||||
if reconnect || !wasLoaded {
|
if next.certFatal != "" {
|
||||||
s.fatal = ""
|
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()
|
s.closeLogLocked()
|
||||||
if next.enableLogging {
|
if next.enableLogging {
|
||||||
s.openLog()
|
s.openLog()
|
||||||
@@ -98,7 +109,8 @@ func (s *Station) kickReconnect() {
|
|||||||
s.cancelDial()
|
s.cancelDial()
|
||||||
}
|
}
|
||||||
if s.conn != nil {
|
if s.conn != nil {
|
||||||
s.conn.Close()
|
// Close unblocks the session read so Run can dial again.
|
||||||
|
_ = s.conn.Close()
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case s.apply <- struct{}{}:
|
case s.apply <- struct{}{}:
|
||||||
@@ -139,7 +151,7 @@ func readForm(path string) (ConfigForm, string, bool) {
|
|||||||
}
|
}
|
||||||
return form, fmt.Errorf("the file could not be read: %w", err).Error(), false
|
return form, fmt.Errorf("the file could not be read: %w", err).Error(), false
|
||||||
}
|
}
|
||||||
out, err := parseConfig(path, body)
|
out, err := parseConfig(path, body, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out.form, err.Error(), false
|
return out.form, err.Error(), false
|
||||||
}
|
}
|
||||||
@@ -170,9 +182,6 @@ func validateForm(configPath string, form ConfigForm) (loaded, ConfigForm, error
|
|||||||
return loaded{}, form, err
|
return loaded{}, form, err
|
||||||
}
|
}
|
||||||
cfg = applyCertificate(cfg, shape, configPath)
|
cfg = applyCertificate(cfg, shape, configPath)
|
||||||
if cfg.certFatal != "" {
|
|
||||||
return loaded{}, form, errors.New(cfg.certFatal)
|
|
||||||
}
|
|
||||||
cfg = applyLog(cfg, shape)
|
cfg = applyLog(cfg, shape)
|
||||||
stored := ConfigForm{
|
stored := ConfigForm{
|
||||||
Host: cfg.host,
|
Host: cfg.host,
|
||||||
@@ -272,11 +281,16 @@ func createExclusive(path string, body []byte) error {
|
|||||||
}
|
}
|
||||||
_, werr := f.Write(body)
|
_, werr := f.Write(body)
|
||||||
cerr := f.Close()
|
cerr := f.Close()
|
||||||
if werr != nil {
|
if werr != nil || cerr != nil {
|
||||||
return werr
|
os.Remove(path)
|
||||||
}
|
if werr != nil {
|
||||||
if cerr != nil {
|
return werr
|
||||||
|
}
|
||||||
return cerr
|
return cerr
|
||||||
}
|
}
|
||||||
return os.Chmod(path, 0o600)
|
if err := os.Chmod(path, 0o600); err != nil {
|
||||||
|
os.Remove(path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,107 @@ func TestConfigStaysClosedWhilePushToTalkIsHeld(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCertificateProblemStaysOnTheStation(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||||
|
body := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"key\",\"talkGroup\":20,\"certificateDir\":\"\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
||||||
|
if err := os.WriteFile(path, body, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
st, err := station.New(station.Options{ConfigPath: path})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(st.Close)
|
||||||
|
got := st.State()
|
||||||
|
if got.ConfigOpen || got.NeedsConfig || got.Detail != "certificateDir is empty" || got.Connection != "Disconnected" {
|
||||||
|
t.Fatalf("State = %+v, want the station window with the certificate detail", got)
|
||||||
|
}
|
||||||
|
if st.Cancel() {
|
||||||
|
t.Fatal("Cancel() quit after a certificate-directory problem")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenConfigDoesNotRewriteAHandEdit(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
path := filepath.Join(dir, "funknetz.json")
|
||||||
|
body := []byte(fullConfig("127.0.0.1", 5308, "DL1ABC", "key", 20, filepath.Join(dir, "funknetz.log")))
|
||||||
|
if err := os.WriteFile(path, body, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
st, err := station.New(station.Options{ConfigPath: path})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(st.Close)
|
||||||
|
edited := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"key\",\"talkGroup\":20,\"enableLogging\":false,\"logFile\":\"" + filepath.Join(dir, "funknetz.log") + "\"}\n")
|
||||||
|
if err := os.WriteFile(path, edited, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
st.OpenConfig()
|
||||||
|
if st.Cancel() {
|
||||||
|
t.Fatal("Cancel() quit")
|
||||||
|
}
|
||||||
|
got, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(got) != string(edited) {
|
||||||
|
t.Fatalf("file = %s, want the hand-edit left unchanged", got)
|
||||||
|
}
|
||||||
|
if st.State().Callsign != "DL1ABC" || st.State().ConfigOpen {
|
||||||
|
t.Fatalf("State = %+v, want the loaded session and a closed Config window", st.State())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveEmptyCertificateDirDoesNotConnect(t *testing.T) {
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
port := ln.Addr().(*net.TCPAddr).Port
|
||||||
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||||
|
st, err := station.New(station.Options{ConfigPath: path})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(st.Close)
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
defer cancel()
|
||||||
|
go func() {
|
||||||
|
// Run's error is observed through State, which is the station seam.
|
||||||
|
_ = st.Run(ctx)
|
||||||
|
}()
|
||||||
|
err = st.Save(station.ConfigForm{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: port,
|
||||||
|
Callsign: "DL1ABC",
|
||||||
|
AuthKey: "key",
|
||||||
|
TalkGroup: 20,
|
||||||
|
CertificateDir: "",
|
||||||
|
LogFile: "/tmp/funknetz.log",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := st.State()
|
||||||
|
if got.ConfigOpen || got.Detail != "certificateDir is empty" || got.Connection != "Disconnected" {
|
||||||
|
t.Fatalf("State = %+v, want the window closed and the certificate detail", got)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm() != 0o600 {
|
||||||
|
t.Fatalf("mode = %o, want 600", info.Mode().Perm())
|
||||||
|
}
|
||||||
|
ln.(*net.TCPListener).SetDeadline(time.Now().Add(200 * time.Millisecond))
|
||||||
|
if conn, err := ln.Accept(); err == nil {
|
||||||
|
conn.Close()
|
||||||
|
t.Fatal("opened a socket")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoggingOnlySaveKeepsTheSession(t *testing.T) {
|
func TestLoggingOnlySaveKeepsTheSession(t *testing.T) {
|
||||||
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
||||||
logPath := filepath.Join(t.TempDir(), "station.log")
|
logPath := filepath.Join(t.TempDir(), "station.log")
|
||||||
@@ -253,7 +354,10 @@ func TestLoggingSaveDoesNotClearAFatalStop(t *testing.T) {
|
|||||||
st.Fail("could not open the audio device")
|
st.Fail("could not open the audio device")
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
go func() { _ = st.Run(ctx) }()
|
go func() {
|
||||||
|
// Run's error is observed through State, which is the station seam.
|
||||||
|
_ = st.Run(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
st.OpenConfig()
|
st.OpenConfig()
|
||||||
form := st.Config().Form
|
form := st.Config().Form
|
||||||
@@ -297,7 +401,10 @@ func TestSaveWriteFailureKeepsTheSession(t *testing.T) {
|
|||||||
if err := os.Chmod(dir, 0o555); err != nil {
|
if err := os.Chmod(dir, 0o555); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { _ = os.Chmod(dir, 0o755) })
|
t.Cleanup(func() {
|
||||||
|
// Restore the directory so the test temp dir can be removed.
|
||||||
|
_ = os.Chmod(dir, 0o755)
|
||||||
|
})
|
||||||
in.st.OpenConfig()
|
in.st.OpenConfig()
|
||||||
form := in.st.Config().Form
|
form := in.st.Config().Form
|
||||||
form.TalkGroup = 21
|
form.TalkGroup = 21
|
||||||
@@ -393,8 +500,7 @@ func connStillOpen(conn net.Conn) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var ne net.Error
|
if ne, ok := errors.AsType[net.Error](err); ok && ne.Timeout() {
|
||||||
if errors.As(err, &ne) && ne.Timeout() {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
+2
-1
@@ -62,7 +62,8 @@ func (s *Station) Run(ctx context.Context) error {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if stale {
|
if stale {
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
conn.Close()
|
// This dial used the config from before Save.
|
||||||
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
s.drainApply()
|
s.drainApply()
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user