From 81b06a16a03147dada4f22081c852353e8d05463 Mon Sep 17 00:00:00 2001 From: Dieter Lang Date: Wed, 12 Aug 2026 15:57:46 +0200 Subject: [PATCH] Version 0.0.8 - Virtuelle Ports validieren und GUI verbessern --- CHANGELOG.md | 38 ++ README.md | 419 +++++++++++++----- cmd/rs2322tcp-client/main.go | 93 +++- internal/client/application.go | 80 ++-- internal/client/runtime.go | 4 + internal/client/virtual_port_linux.go | 74 +++- internal/client/virtual_port_linux_test.go | 205 ++++++++- internal/client/virtual_port_validation.go | 86 ++++ .../client/virtual_port_validation_linux.go | 184 ++++++++ .../client/virtual_port_validation_test.go | 283 ++++++++++++ internal/gui/app.go | 87 +++- 11 files changed, 1386 insertions(+), 167 deletions(-) create mode 100644 internal/client/virtual_port_validation.go create mode 100644 internal/client/virtual_port_validation_linux.go create mode 100644 internal/client/virtual_port_validation_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac65cc..2f1d720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert. +## [0.0.8] - 2026-08-12 + +### Added + +- Prüfung der lokal vorhandenen virtuellen Ports beim Client-Start +- Abgleich der vorhandenen symbolischen Links mit den in `client.json` + definierten virtuellen Schnittstellen +- Erkennung von in `client.json` definierten, aber lokal nicht vorhandenen + virtuellen Ports +- Erkennung von lokal vorhandenen, aber nicht in `client.json` definierten + virtuellen Ports +- Verständlicher Fehlerdialog bei einer abweichenden virtuellen + Portkonfiguration +- Beenden des Clients nach Bestätigung einer fehlerhaften + Portkonfiguration + +### Changed + +- Virtuelle Ports werden entsprechend der in `client.json` festgelegten + Portnummern gezielt geöffnet +- Die Verwaltung virtueller Ports arbeitet weiterhin ohne Root-Rechte +- Der Fehlerdialog verwendet einen einzelnen OK-Button +- Allgemeine Schriftgröße der grafischen Benutzeroberfläche auf 22 erhöht +- Hauptfenster des Clients auf 800 × 600 Pixel vergrößert + +### Tests + +- Prüfung vorhandener und konfigurierter virtueller Ports erfolgreich getestet +- Fehlender virtueller Port erfolgreich erkannt und im Fehlerdialog angezeigt +- Zusätzlicher, nicht konfigurierter virtueller Port erfolgreich erkannt +- Clientstart nach Korrektur der virtuellen Portkonfiguration erfolgreich getestet +- Gerätezuordnung nach Neustart des Clients erfolgreich aus `client.json` + geladen +- `go test ./...` erfolgreich +- `go test -race ./internal/client` erfolgreich +- `go test -race ./internal/gui` erfolgreich +- `git diff --check` erfolgreich + ## [0.0.7] - 2026-08-12 ### Added diff --git a/README.md b/README.md index 3e56c34..f133379 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ rs2322tcp-client │ Funkgerät Rotor ``` -## Aktueller Server-Datenpfad +## Aktueller Datenpfad -Die serverseitige TCP-/RS232-Verbindung ist implementiert: +Der serverseitige TCP-/RS232-Datenpfad ist implementiert. ```text TCP-Control @@ -85,32 +85,86 @@ TCP ───────────────► RS232 TCP ◄────────────── RS232 ``` +Auch der Client-Datenpfad ist implementiert: + +```text +Virtueller serieller Port + │ + ▼ + rs2322tcp-client + │ + │ TCP-Control + ▼ + Session + │ + │ TCP-Data + ▼ + rs2322tcp-server + │ + ▼ + reale serielle + Schnittstelle + │ + ▼ + Gerät +``` + +Der Client kann mehrere konfigurierte virtuelle serielle Schnittstellen +gleichzeitig verwalten. + ## Client Der Client soll gleichberechtigt unter folgenden Betriebssystemen eingesetzt werden können: -- Windows - Linux +- Windows -Die plattformspezifische Bereitstellung der virtuellen seriellen -Schnittstelle wird vom gemeinsamen Client-Kern getrennt. +Die gemeinsame Client-Logik ist von der plattformspezifischen +Bereitstellung der virtuellen seriellen Schnittstelle getrennt. -Unter Linux ist die virtuelle serielle Schnittstelle inzwischen über -einen direkt aus Go erzeugten PTY implementiert. Die Implementierung -verwendet `/dev/ptmx` und stellt den zugehörigen PTY-Slave unter -`/dev/pts/N` bereit. +Der Client besteht im Wesentlichen aus: + +- Control-Verbindung zum Server +- Verwaltung der vom Server angebotenen Geräte +- Runtime zur Verwaltung mehrerer virtueller Ports +- virtuellen seriellen Schnittstellen +- TCP-Data-Connections +- bidirektionalen Bridges zwischen virtuellem Port und TCP-Verbindung +- grafischer Benutzeroberfläche + +### Linux + +Unter Linux ist die virtuelle serielle Schnittstelle über einen direkt +aus Go erzeugten PTY implementiert. + +Die Implementierung verwendet `/dev/ptmx` und stellt den zugehörigen +PTY-Slave unter `/dev/pts/N` bereit. Die PTY-Schnittstelle wird im Raw Mode betrieben. Dadurch werden die übertragenen Daten als transparenter Byte-Strom behandelt. Die allgemeine Client-Logik verwendet dafür die Abstraktion -`VirtualSerial`. Die Linux-spezifische Implementierung ist von dieser -Abstraktion getrennt. +`VirtualSerial`. + +Die Linux-spezifische Implementierung ist von dieser Abstraktion +getrennt. Für die PTY-Funktion ist keine externe Laufzeitabhängigkeit wie `socat` erforderlich. +### Windows + +Die gemeinsame Client-Architektur ist bereits auf eine +plattformabhängige Implementierung der virtuellen seriellen +Schnittstelle vorbereitet. + +Die Windows-Implementierung der virtuellen seriellen Schnittstelle ist +noch nicht vollständig implementiert. + +Der spätere Windows-Client soll dieselbe gemeinsame Client- und +Konfigurationslogik verwenden. + ## Server Der Server ist zunächst für den Betrieb auf einem Raspberry Pi 5 @@ -159,18 +213,206 @@ untersucht werden. Client und Server erhalten jeweils eine eigene JSON-Konfiguration. -Über die Konfiguration werden unter anderem festgelegt: +### Client-Konfiguration -- Netzwerkparameter -- serielle Schnittstelle -- Baudrate -- Datenbits -- Parität -- Stopbits -- Bezeichnung des Gerätes -- weitere für die jeweilige Schnittstelle erforderliche Parameter +Über die Client-Konfiguration werden unter anderem festgelegt: -Die konkrete Konfigurationsstruktur befindet sich in der Entwicklung. +- Server-Adresse +- Server-Port +- reservierter Bereich der virtuellen seriellen Schnittstellen +- Zuordnung virtueller Schnittstellen zu Remote-Geräten + +Beispiel: + +```json +{ + "server": { + "address": "127.0.0.1", + "port": 5000 + }, + "virtual_port_range": { + "first": 100, + "last": 199 + }, + "virtual_ports": [ + { + "port": "/dev/ttyUSB100", + "remote_device": "radio" + }, + { + "port": "/dev/ttyUSB101", + "remote_device": "rotor" + } + ] +} +``` + +Der konfigurierte Bereich + +```json +"virtual_port_range": { + "first": 100, + "last": 199 +} +``` + +definiert den für `rs2322tcp` vorgesehenen Bereich virtueller +Schnittstellen. + +Die konkrete Zuordnung wird über `virtual_ports` festgelegt. + +Beispielsweise: + +```text +/dev/ttyUSB100 -> radio +/dev/ttyUSB101 -> rotor +``` + +Ein leerer Wert für `remote_device` kennzeichnet eine freie virtuelle +Schnittstelle. + +Virtuelle Schnittstellen bleiben dadurch auch ohne aktuelle +Gerätezuordnung in der Client-Konfiguration erhalten. + +### Gerätezuordnung + +Die vom Server angebotenen Geräte können über die grafische +Benutzeroberfläche den konfigurierten virtuellen Schnittstellen +zugeordnet werden. + +Dabei gelten folgende Regeln: + +- Eine virtuelle Schnittstelle kann nur einem Gerät zugeordnet werden. +- Ein Gerät kann nicht gleichzeitig mehreren virtuellen Schnittstellen + zugeordnet werden. +- Eine bereits belegte Schnittstelle kann auf ihre aktuelle Zuordnung + oder auf „nicht verbunden“ gesetzt werden. +- Freie virtuelle Schnittstellen werden automatisch zur Auswahl + angeboten. +- Änderungen werden zunächst nur im Arbeitsspeicher gehalten. +- Beim Schließen des Gerätezuordnungsfensters werden die Änderungen in + `client.json` gespeichert. +- Wird das Hauptfenster geschlossen, während ungespeicherte Änderungen + vorhanden sind, wird der Anwender darauf hingewiesen. + +## Prüfung der virtuellen seriellen Schnittstellen + +Beim Start des Linux-Clients werden die in `client.json` definierten +virtuellen Schnittstellen mit den tatsächlich vorhandenen öffentlichen +symbolischen Links verglichen. + +Dabei werden ausschließlich zwei Listen miteinander verglichen: + +```text +vorhandene symbolische Links + ↕ +definierte virtuelle Ports +``` + +Es werden insbesondere folgende Abweichungen erkannt: + +- Ein in `client.json` definierter Port besitzt keinen entsprechenden + öffentlichen Symlink. +- Ein öffentlicher Symlink ist vorhanden, aber nicht in `client.json` + eingetragen. + +Bei einer Abweichung wird der Clientstart abgebrochen und dem Anwender +ein verständlicher Hinweis angezeigt. + +Der Client legt fehlende öffentliche Links nicht automatisch an. + +Damit bleibt die Systemkonfiguration unter der Kontrolle des Anwenders. + +Die Prüfung selbst verändert keine Systemdateien und benötigt keine +Root-Rechte. + +## Manuelle Einrichtung der virtuellen seriellen Ports unter Linux + +Die öffentlichen virtuellen seriellen Ports werden unter Linux einmalig +eingerichtet. + +Der laufende `rs2322tcp-client` benötigt anschließend keine Root-Rechte. + +Zunächst wird das Verzeichnis für die internen virtuellen Links angelegt: + +```bash +mkdir -p ~/.rs2322tcp/virtual +``` + +Anschließend werden die öffentlichen `/dev/ttyUSBxxx`-Links einmalig +mit administrativen Rechten angelegt. + +Beispiel: + +```bash +sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB100" /dev/ttyUSB100 +sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB101" /dev/ttyUSB101 +``` + +Die Zuordnung ist: + +```text +/dev/ttyUSB100 -> ~/.rs2322tcp/virtual/ttyUSB100 -> PTY +/dev/ttyUSB101 -> ~/.rs2322tcp/virtual/ttyUSB101 -> PTY +``` + +Die äußeren Links unter `/dev` gehören dabei `root`. Das ist beabsichtigt. + +Der Client selbst läuft anschließend als normaler Benutzer. + +Die internen Links unter: + +```text +~/.rs2322tcp/virtual/ +``` + +werden vom Client auf die jeweils verwendeten PTYs gesetzt. + +Die Einrichtung eines neuen virtuellen Ports besteht daher aus: + +1. Einen freien Port aus dem konfigurierten Bereich auswählen. +2. Den öffentlichen `/dev/ttyUSBxxx`-Symlink einmalig anlegen. +3. Den Port in `client.json` eintragen. +4. Den Client starten. +5. Die Gerätezuordnung gegebenenfalls über die GUI vornehmen. + +Der Client prüft beim Start, ob die Konfiguration und die vorhandenen +Symlinks übereinstimmen. + +Die konkrete Einrichtung ist Linux-spezifisch. Für Windows wird später +eine eigene plattformspezifische Lösung verwendet. + +## Virtuelle serielle Schnittstelle unter Linux + +Für den Linux-Client wird die virtuelle serielle Schnittstelle direkt +über den Linux-PTY-Mechanismus erzeugt. + +Dabei entsteht ein PTY-Paar: + +```text +/dev/ptmx + │ + ▼ +PTY Master + │ + ↕ +PTY Slave + │ + ▼ +/dev/pts/N +``` + +Der PTY-Slave wird über den vom Anwender eingerichteten virtuellen +`/dev/ttyUSBxxx`-Symlink erreichbar. + +Der `rs2322tcp-client` verwendet den PTY-Master und transportiert die +Bytes über TCP zum Server. + +Die PTY-Implementierung arbeitet im Raw Mode und wurde mit einem +vollständigen Bytebereich von `0x00` bis `0xFF` geprüft. + +Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte +erfolgreich getestet. ## Diagnose und Logging @@ -267,7 +509,7 @@ Rotor: 1 Stopbit ``` -Der Client stellt dem Anwender dafür den virtuellen Port: +Der Client stellt dem Anwender dafür beispielsweise den virtuellen Port: ```text /dev/ttyUSB101 @@ -326,80 +568,6 @@ Damit wurde die bidirektionale Übertragung zwischen dem virtuellen seriellen Port des Clients und dem realen SPID-Rot2Prog über TCP erfolgreich nachgewiesen. -## Manuelle Einrichtung der virtuellen seriellen Ports - -Die virtuellen seriellen Ports werden vor dem Start des Clients manuell -eingerichtet. Dadurch muss der `rs2322tcp-client` selbst nicht als -`root` bzw. mit `sudo` laufen. - -Zunächst das Verzeichnis für die internen virtuellen Links anlegen: - -```bash -mkdir -p ~/.rs2322tcp/virtual -``` - -Anschließend werden die öffentlichen `/dev/ttyUSBxxx`-Links einmalig -mit administrativen Rechten angelegt: - -```bash -sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB100" /dev/ttyUSB100 -sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB101" /dev/ttyUSB101 -``` - -Die Zuordnung ist: - -```text -/dev/ttyUSB100 -> ~/.rs2322tcp/virtual/ttyUSB100 -> PTY -/dev/ttyUSB101 -> ~/.rs2322tcp/virtual/ttyUSB101 -> PTY -``` - -Die äußeren Links unter `/dev` gehören dabei `root`. Das ist beabsichtigt. -Der Client selbst läuft anschließend als normaler Benutzer. - -Die internen Links unter `~/.rs2322tcp/virtual/` werden vom Client auf die -jeweils verwendeten PTYs gesetzt. - -Die beiden virtuellen Ports werden in der Client-Konfiguration den -Geräten zugeordnet: - -```text -/dev/ttyUSB100 -> radio -/dev/ttyUSB101 -> rotor -``` - -Die manuelle Einrichtung muss nur erfolgen, wenn die `/dev/ttyUSB100`- -und `/dev/ttyUSB101`-Links noch nicht vorhanden sind. - -## Virtuelle serielle Schnittstelle unter Linux - -Für den Linux-Client wird die virtuelle serielle Schnittstelle direkt -über den Linux-PTY-Mechanismus erzeugt. - -Dabei entsteht ein PTY-Paar: - -```text -/dev/ptmx - │ - ▼ -PTY Master - │ - ↕ -PTY Slave - │ - ▼ -/dev/pts/N -``` - -Die externe Hersteller-Software arbeitet mit dem PTY-Slave `/dev/pts/N`. -Der `rs2322tcp-client` verwendet den PTY-Master und transportiert die -Bytes über TCP zum Server. - -Die PTY-Implementierung arbeitet im Raw Mode und wurde mit einem -vollständigen Bytebereich von `0x00` bis `0xFF` geprüft. - -Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte -erfolgreich getestet. - ## Geräte-Simulator Das Programm `rs2322tcp-device-sim` dient als Simulator für ein an der @@ -458,13 +626,13 @@ Auch Groß- und Kleinschreibung kann gemischt werden: 0a FF 01 b7 ``` -Ungültige Eingaben wie +Ungültige Eingaben wie: ```text GG ``` -oder +oder: ```text 4 @@ -540,8 +708,7 @@ empfangenen Bytes lediglich an. ## Externe USB-to-Seriell-Adapter -Für den späteren praktischen Einsatz sind USB-to-Seriell-Adapter -vorgesehen. +Für den praktischen Einsatz sind USB-to-Seriell-Adapter vorgesehen. Unter Linux werden solche Adapter normalerweise als serielle Gerätedateien bereitgestellt, beispielsweise: @@ -578,12 +745,28 @@ go test -race ./... ``` Der Race Detector wird eingesetzt, um Probleme bei der nebenläufigen -Verarbeitung von Sessions, Data-Listenern, DataConnections und Client- -Komponenten zu erkennen. +Verarbeitung von Sessions, Data-Listenern, DataConnections und +Client-Komponenten zu erkennen. Die Linux-PTY-Komponente wurde mit einem vollständigen Bytebereich von `0x00` bis `0xFF` geprüft. +Die Verwaltung der virtuellen Ports verfügt über Tests für: + +- automatische Portreservierung +- gezielte Reservierung einer konfigurierten Portnummer +- Freigabe virtueller Ports +- Erstellung und Entfernung der internen PTY-Links +- Abgleich vorhandener virtueller Ports mit der Client-Konfiguration + +Die Gerätezuordnung verfügt über Tests für: + +- Zuordnung von Geräten zu virtuellen Ports +- Freigabe virtueller Ports +- Vermeidung doppelter Zuordnungen +- Speicherung der Konfiguration +- Laden der gespeicherten Zuordnung + Der Geräte-Simulator verfügt zusätzlich über automatisierte Tests für die Hex-Konvertierung und die Behandlung ungültiger Eingaben. @@ -644,6 +827,9 @@ Aktuell werden folgende Builds erzeugt: - Linux armv7 -- Server - Linux armv6 -- Server +Die gemeinsame Client-Architektur ist für eine spätere +Windows-Unterstützung vorbereitet. + ## Versionierung Die Versionierung erfolgt über Git. @@ -654,17 +840,20 @@ Build-Datum erfasst. Release-Versionen werden über Git-Tags gekennzeichnet. -Aktueller Stand: +Aktueller Entwicklungsstand: -- `v0.0.1` – erste Projektversion +- `v0.0.1` – Projektgrundlage und Buildsystem - `v0.0.2` – Client-Control- und Data-Verbindungen - `v0.0.3` – Linux-PTY-Unterstützung und Geräte-Simulator -- `v0.0.4` – in Vorbereitung: interaktive TX-Funktion und automatisierte - Tests des Geräte-Simulators +- `v0.0.4` – interaktive TX-Funktion des Geräte-Simulators +- `v0.0.5` – konfigurierbare lokale virtuelle serielle Ports +- `v0.0.6` – Client-Runtime und Serial-Bridge +- `0.0.7` – Geräteerkennung und grafische Gerätezuordnung +- `0.0.8` – Validierung der virtuellen Ports und Überarbeitung der Client-GUI ## Entwicklungsstand -Das Projekt befindet sich weiterhin in der frühen Entwicklungsphase. +Das Projekt befindet sich weiterhin in der Entwicklungsphase. Implementiert und getestet sind derzeit: @@ -677,7 +866,8 @@ Implementiert und getestet sind derzeit: - Konfiguration der seriellen Parameter - Client-Control-Verbindung - Client-DataConnection -- Client-Integrationstests +- Client-Runtime zur Verwaltung mehrerer virtueller Ports +- gezielte Zuordnung konfigurierter virtueller Portnummern - Linux-PTY-Abstraktion - Linux-PTY im Raw Mode - automatisierter bidirektionaler 256-Byte-PTY-Test @@ -686,13 +876,22 @@ Implementiert und getestet sind derzeit: - eigenständiger Geräte-Simulator mit RX-Hexdump - interaktive TX-Hex-Eingabe des Geräte-Simulators - automatisierte Tests der Hex-Konvertierung +- grafische Gerätezuordnung im Client +- Speicherung der Gerätezuordnung in `client.json` +- Laden der Gerätezuordnung beim Neustart +- Validierung der vorhandenen virtuellen Ports beim Clientstart +- Abgleich von vorhandenen Symlinks und konfigurierten virtuellen Ports +- Linux-Portverwaltung ohne Root-Rechte im laufenden Client +- verständlicher Fehlerdialog bei fehlerhafter Portkonfiguration +- anpassbare allgemeine Schriftgröße der GUI +- überarbeitete Fenstergrößen der GUI Noch nicht vollständig implementiert sind insbesondere: -- die vollständige Anbindung der virtuellen seriellen Schnittstelle an - den produktiven Client-Anwendungsdatenpfad - die Windows-Implementierung der virtuellen seriellen Schnittstelle - automatische Geräteantworten und Protokollsimulation +- weitere produktive Funktionen des Clients und der GUI +- weiterführende Diagnose- und Logging-Funktionen ## Lizenz diff --git a/cmd/rs2322tcp-client/main.go b/cmd/rs2322tcp-client/main.go index 941eafe..4713208 100644 --- a/cmd/rs2322tcp-client/main.go +++ b/cmd/rs2322tcp-client/main.go @@ -10,9 +10,9 @@ * Einstiegspunkt für den grafischen rs2322tcp-Client. * * Der Client wird als eigenständige Desktop-Anwendung ausgeführt. Die - * grafische Oberfläche basiert auf Fyne und bildet sowohl die - * Statusanzeige als auch später die Konfiguration der lokalen virtuellen - * seriellen Ports ab. + * grafische Oberfläche basiert auf Fyne und bildet sowohl die Statusanzeige + * als auch später die Konfiguration der lokalen virtuellen seriellen + * Ports ab. * * Die eigentliche Client- und Runtime-Logik bleibt in den internen * Packages gekapselt. Diese Datei ist bewusst auf den Programmstart @@ -23,24 +23,93 @@ package main import ( "fmt" - + "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/theme" + "image/color" "git.lang-dieter.de/rs2322tcp/internal/gui" "git.lang-dieter.de/rs2322tcp/internal/version" ) -func main() { - fmt.Printf("rs2322tcp-client %s\n", version.Version) +/////////////////////////////////////////////////////////////////////////////// +// Application theme +/////////////////////////////////////////////////////////////////////////////// - application := app.NewWithID("git.lang-dieter.de.rs2322tcp.client") +// applicationTheme keeps the standard Fyne theme and increases the general +// text size for better readability. +// +// All other colors, fonts, icons and dimensions remain unchanged. +type applicationTheme struct{} + +// Ensure applicationTheme implements fyne.Theme. +var _ fyne.Theme = (*applicationTheme)(nil) + +// Color returns the standard Fyne theme color. +func (t *applicationTheme) Color( + name fyne.ThemeColorName, + variant fyne.ThemeVariant, +) color.Color { + return theme.DefaultTheme().Color(name, variant) +} + +// Font returns the standard Fyne theme font. +func (t *applicationTheme) Font( + style fyne.TextStyle, +) fyne.Resource { + return theme.DefaultTheme().Font(style) +} + +// Icon returns the standard Fyne theme icon. +func (t *applicationTheme) Icon( + name fyne.ThemeIconName, +) fyne.Resource { + return theme.DefaultTheme().Icon(name) +} + +// Size returns the standard Fyne theme size, except for normal text. +// +// Normal text is increased from the Fyne default to 22 px for better +// readability. +func (t *applicationTheme) Size( + name fyne.ThemeSizeName, +) float32 { + if name == theme.SizeNameText { + return 22 + } + + return theme.DefaultTheme().Size(name) +} + +/////////////////////////////////////////////////////////////////////////////// +// Main +/////////////////////////////////////////////////////////////////////////////// + +func main() { + fmt.Printf( + "rs2322tcp-client %s\n", + version.Version, + ) + + application := app.NewWithID( + "git.lang-dieter.de.rs2322tcp.client", + ) + + // Use the standard Fyne theme with a larger general text size. + application.Settings().SetTheme( + &applicationTheme{}, + ) guiApplication, err := gui.NewApp( application, "./configs/client.json", ) if err != nil { - fmt.Printf("GUI konnte nicht gestartet werden: %v\n", err) + fmt.Printf( + "GUI konnte nicht gestartet werden: %v\n", + err, + ) + return } @@ -54,9 +123,11 @@ func main() { // Beim Schließen des Hauptfensters wird die technische Client- // Anwendung beendet. Dadurch werden Runtime, Bridges, virtuelle // Ports und die Serververbindung sauber geschlossen. - guiApplication.Window().SetOnClosed(func() { - guiApplication.Close() - }) + guiApplication.Window().SetOnClosed( + func() { + guiApplication.Close() + }, + ) // ShowAndRun zeigt das Fenster und startet den Fyne-Eventloop. guiApplication.ShowAndRun() diff --git a/internal/client/application.go b/internal/client/application.go index 602519f..19c5a52 100644 --- a/internal/client/application.go +++ b/internal/client/application.go @@ -19,6 +19,9 @@ * Konfiguration laden * | * v + * Lokale virtuelle Ports prüfen + * | + * v * Server verbinden * | * v @@ -93,8 +96,8 @@ func NewApplication(configFile string) (*Application, error) { // Start /////////////////////////////////////////////////////////////////////////////// -// Start loads the client configuration, connects to the server and starts -// the client Runtime. +// Start loads the client configuration, validates the locally available +// virtual ports, connects to the server and starts the client Runtime. // // The Runtime is started asynchronously because Runtime.Run blocks while // the configured virtual serial connections are active. @@ -118,6 +121,16 @@ func (a *Application) Start() error { return fmt.Errorf("load client configuration: %w", err) } + // Before connecting to the server, make sure that the virtual ports + // configured in client.json match the locally installed virtual ports. + // + // This check is deliberately performed before New(address), so an + // invalid local virtual-port configuration prevents the client from + // starting at all. + if err := validateLocalVirtualPorts(cfg); err != nil { + return err + } + address := fmt.Sprintf( "%s:%d", cfg.Server.Address, @@ -234,6 +247,10 @@ func (a *Application) Close() error { return firstErr } +/////////////////////////////////////////////////////////////////////////////// +// Configuration +/////////////////////////////////////////////////////////////////////////////// + // Config returns a copy of the currently loaded client configuration. // // The configuration is available after NewApplication, even when the @@ -260,6 +277,36 @@ func (a *Application) Config() *config.ClientConfig { return &cfg } +// SaveConfig saves the supplied client configuration to the configuration +// file used by this Application. +// +// After a successful save the supplied configuration also becomes the +// current in-memory configuration. +// +// The running Runtime is not restarted automatically. A subsequent +// Reconnect or application restart will use the saved configuration. +func (a *Application) SaveConfig( + cfg *config.ClientConfig, +) error { + if a == nil { + return fmt.Errorf("application is nil") + } + + if cfg == nil { + return fmt.Errorf("client configuration is nil") + } + + if err := config.SaveClient(a.configFile, cfg); err != nil { + return err + } + + a.mu.Lock() + a.config = cfg + a.mu.Unlock() + + return nil +} + /////////////////////////////////////////////////////////////////////////////// // Devices /////////////////////////////////////////////////////////////////////////////// @@ -298,32 +345,3 @@ func (a *Application) Running() bool { return a.runtime != nil && a.client != nil } -// SaveConfig saves the supplied client configuration to the configuration -// file used by this Application. -// -// After a successful save the supplied configuration also becomes the -// current in-memory configuration. -// -// The running Runtime is not restarted automatically. A subsequent -// Reconnect or application restart will use the saved configuration. -func (a *Application) SaveConfig( - cfg *config.ClientConfig, -) error { - if a == nil { - return fmt.Errorf("application is nil") - } - - if cfg == nil { - return fmt.Errorf("client configuration is nil") - } - - if err := config.SaveClient(a.configFile, cfg); err != nil { - return err - } - - a.mu.Lock() - a.config = cfg - a.mu.Unlock() - - return nil -} diff --git a/internal/client/runtime.go b/internal/client/runtime.go index d24f7bb..382f9c0 100644 --- a/internal/client/runtime.go +++ b/internal/client/runtime.go @@ -99,6 +99,10 @@ func (r *Runtime) Run( } for _, virtualPortConfig := range cfg.VirtualPorts { + if virtualPortConfig.RemoteDevice == "" { + continue + } + bridge, _, _, err := connectBridge( r.client, r.manager, diff --git a/internal/client/virtual_port_linux.go b/internal/client/virtual_port_linux.go index 8467a04..42e820d 100644 --- a/internal/client/virtual_port_linux.go +++ b/internal/client/virtual_port_linux.go @@ -23,6 +23,8 @@ import ( "fmt" "os" "path/filepath" + + "git.lang-dieter.de/rs2322tcp/internal/config" ) const ( @@ -130,7 +132,7 @@ func removeVirtualPortLink(portPath string) error { if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf( - "remove virtual port link %q: %w", + "remove existing virtual port link %q: %w", linkPath, err, ) @@ -138,3 +140,73 @@ func removeVirtualPortLink(portPath string) error { return nil } + +// existingVirtualPortLinks returns all public virtual-port paths that +// currently exist as symbolic links. +// +// Only ports inside the configured virtual-port range are considered. +// The public /dev/ttyUSBxxx links are only inspected; they are never +// created, changed, or removed by this function. +// +// This check only requires read access and therefore does not require +// root privileges. +func existingVirtualPortLinks( + cfg config.VirtualPortRangeConfig, +) ([]string, error) { + return existingVirtualPortLinksInDirectory( + cfg, + "/dev", + ) +} + +// existingVirtualPortLinksInDirectory returns all virtual-port paths in +// directory that currently exist as symbolic links. +// +// This helper is deliberately separated from existingVirtualPortLinks so +// the filesystem inspection can be tested without modifying /dev. +func existingVirtualPortLinksInDirectory( + cfg config.VirtualPortRangeConfig, + directory string, +) ([]string, error) { + if directory == "" { + return nil, fmt.Errorf("virtual port directory is empty") + } + + manager := NewVirtualPortManager(cfg) + if manager == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + + links := make([]string, 0) + + for number := cfg.First; number <= cfg.Last; number++ { + portName := filepath.Base( + manager.PortPath(number), + ) + portPath := filepath.Join(directory, portName) + + info, err := os.Lstat(portPath) + if err != nil { + if os.IsNotExist(err) { + continue + } + + return nil, fmt.Errorf( + "inspect virtual port %q: %w", + portPath, + err, + ) + } + + if info.Mode()&os.ModeSymlink == 0 { + continue + } + + links = append( + links, + filepath.Join(directory, portName), + ) + } + + return links, nil +} diff --git a/internal/client/virtual_port_linux_test.go b/internal/client/virtual_port_linux_test.go index 8ff7091..6cdbc82 100644 --- a/internal/client/virtual_port_linux_test.go +++ b/internal/client/virtual_port_linux_test.go @@ -18,7 +18,10 @@ package client import ( "os" "path/filepath" + "reflect" "testing" + + "git.lang-dieter.de/rs2322tcp/internal/config" ) func TestVirtualPortLinkPath(t *testing.T) { @@ -27,7 +30,11 @@ func TestVirtualPortLinkPath(t *testing.T) { t.Fatalf("virtualPortLinkPath() failed: %v", err) } - wantSuffix := filepath.Join(".rs2322tcp", "virtual", "ttyUSB100") + wantSuffix := filepath.Join( + ".rs2322tcp", + "virtual", + "ttyUSB100", + ) if !filepath.IsAbs(path) { t.Fatalf("path is not absolute: %q", path) @@ -91,12 +98,24 @@ func TestSetVirtualPortLinkReplacesExistingLink(t *testing.T) { portPath := "/dev/ttyUSB100" - if err := setVirtualPortLink(portPath, "/dev/pts/2"); err != nil { - t.Fatalf("first setVirtualPortLink() failed: %v", err) + if err := setVirtualPortLink( + portPath, + "/dev/pts/2", + ); err != nil { + t.Fatalf( + "first setVirtualPortLink() failed: %v", + err, + ) } - if err := setVirtualPortLink(portPath, "/dev/pts/7"); err != nil { - t.Fatalf("second setVirtualPortLink() failed: %v", err) + if err := setVirtualPortLink( + portPath, + "/dev/pts/7", + ); err != nil { + t.Fatalf( + "second setVirtualPortLink() failed: %v", + err, + ) } linkPath, err := virtualPortLinkPath(portPath) @@ -146,6 +165,182 @@ func TestVirtualPortLinkPathUsesBaseName(t *testing.T) { } } +func TestExistingVirtualPortLinksNone(t *testing.T) { + tempDir := t.TempDir() + + cfg := config.VirtualPortRangeConfig{ + First: 100, + Last: 102, + } + + got, err := existingVirtualPortLinksInDirectory( + cfg, + tempDir, + ) + if err != nil { + t.Fatalf( + "existingVirtualPortLinksInDirectory() failed: %v", + err, + ) + } + + if len(got) != 0 { + t.Fatalf( + "existingVirtualPortLinksInDirectory() = %v, want empty list", + got, + ) + } +} + +func TestExistingVirtualPortLinksFindsSymlinks(t *testing.T) { + tempDir := t.TempDir() + + cfg := config.VirtualPortRangeConfig{ + First: 100, + Last: 102, + } + + for _, number := range []int{100, 102} { + path := filepath.Join( + tempDir, + "ttyUSB"+itoa(number), + ) + + if err := os.Symlink( + "/dev/pts/"+itoa(number), + path, + ); err != nil { + t.Fatalf( + "create test symlink %q: %v", + path, + err, + ) + } + } + + got, err := existingVirtualPortLinksInDirectory( + cfg, + tempDir, + ) + if err != nil { + t.Fatalf( + "existingVirtualPortLinksInDirectory() failed: %v", + err, + ) + } + + want := []string{ + filepath.Join(tempDir, "ttyUSB100"), + filepath.Join(tempDir, "ttyUSB102"), + } + + if !reflect.DeepEqual(got, want) { + t.Fatalf( + "existingVirtualPortLinksInDirectory() = %v, want %v", + got, + want, + ) + } +} + +func TestExistingVirtualPortLinksIgnoresRegularFile(t *testing.T) { + tempDir := t.TempDir() + + cfg := config.VirtualPortRangeConfig{ + First: 100, + Last: 102, + } + + path := filepath.Join(tempDir, "ttyUSB100") + + if err := os.WriteFile( + path, + []byte("not a symlink"), + 0644, + ); err != nil { + t.Fatalf( + "create regular test file %q: %v", + path, + err, + ) + } + + got, err := existingVirtualPortLinksInDirectory( + cfg, + tempDir, + ) + if err != nil { + t.Fatalf( + "existingVirtualPortLinksInDirectory() failed: %v", + err, + ) + } + + if len(got) != 0 { + t.Fatalf( + "regular file was detected as virtual port: %v", + got, + ) + } +} + +func TestExistingVirtualPortLinksIgnoresOutsideRange(t *testing.T) { + tempDir := t.TempDir() + + cfg := config.VirtualPortRangeConfig{ + First: 100, + Last: 102, + } + + path := filepath.Join(tempDir, "ttyUSB103") + + if err := os.Symlink( + "/dev/pts/103", + path, + ); err != nil { + t.Fatalf( + "create outside-range symlink %q: %v", + path, + err, + ) + } + + got, err := existingVirtualPortLinksInDirectory( + cfg, + tempDir, + ) + if err != nil { + t.Fatalf( + "existingVirtualPortLinksInDirectory() failed: %v", + err, + ) + } + + if len(got) != 0 { + t.Fatalf( + "outside-range symlink was detected: %v", + got, + ) + } +} + +func itoa(number int) string { + const digits = "0123456789" + + if number == 0 { + return "0" + } + + result := "" + + for number > 0 { + result = string(digits[number%10]) + result + number /= 10 + } + + return result +} + func hasPathSuffix(path string, suffix string) bool { return path == suffix || len(path) > len(suffix) && diff --git a/internal/client/virtual_port_validation.go b/internal/client/virtual_port_validation.go new file mode 100644 index 0000000..db324a1 --- /dev/null +++ b/internal/client/virtual_port_validation.go @@ -0,0 +1,86 @@ +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_validation.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Vergleich der in client.json definierten virtuellen Ports mit den + * tatsächlich vorhandenen virtuellen Schnittstellen. + * ============================================================================ + */ +package client + +import "sort" + +/////////////////////////////////////////////////////////////////////////////// +// Virtual port validation +/////////////////////////////////////////////////////////////////////////////// + +// VirtualPortValidationResult contains the differences between configured +// and locally available virtual ports. +type VirtualPortValidationResult struct { + Missing []string + Extra []string +} + +// compareVirtualPorts compares the configured virtual ports with the +// locally available virtual ports. +// +// Missing contains ports configured in client.json that are not available +// locally. +// +// Extra contains locally available ports that are not configured in +// client.json. +// +// The order of both input lists is irrelevant. +func compareVirtualPorts( + configured []string, + available []string, +) VirtualPortValidationResult { + configuredSet := make(map[string]struct{}, len(configured)) + + for _, port := range configured { + configuredSet[port] = struct{}{} + } + + availableSet := make(map[string]struct{}, len(available)) + + for _, port := range available { + availableSet[port] = struct{}{} + } + + result := VirtualPortValidationResult{ + Missing: make([]string, 0), + Extra: make([]string, 0), + } + + for port := range configuredSet { + if _, ok := availableSet[port]; ok { + continue + } + + result.Missing = append( + result.Missing, + port, + ) + } + + for port := range availableSet { + if _, ok := configuredSet[port]; ok { + continue + } + + result.Extra = append( + result.Extra, + port, + ) + } + + sort.Strings(result.Missing) + sort.Strings(result.Extra) + + return result +} diff --git a/internal/client/virtual_port_validation_linux.go b/internal/client/virtual_port_validation_linux.go new file mode 100644 index 0000000..6a93285 --- /dev/null +++ b/internal/client/virtual_port_validation_linux.go @@ -0,0 +1,184 @@ +//go:build linux + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_validation_linux.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Linux-spezifische Prüfung der lokal vorhandenen virtuellen + * rs2322tcp-Schnittstellen gegen die Client-Konfiguration. + * ============================================================================ + */ +package client + +import ( + "fmt" + "strings" + + "git.lang-dieter.de/rs2322tcp/internal/config" +) + +/////////////////////////////////////////////////////////////////////////////// +// Local virtual-port validation +/////////////////////////////////////////////////////////////////////////////// + +// validateLocalVirtualPorts compares the virtual ports defined in +// client.json with the public symbolic links currently available on Linux. +// +// The check is read-only and does not require root privileges. +// +// A mismatch prevents the client from starting. +func validateLocalVirtualPorts( + cfg *config.ClientConfig, +) error { + if cfg == nil { + return fmt.Errorf("client configuration is nil") + } + + configured := make( + []string, + 0, + len(cfg.VirtualPorts), + ) + + for _, virtualPort := range cfg.VirtualPorts { + configured = append( + configured, + virtualPort.Port, + ) + } + + available, err := existingVirtualPortLinks( + cfg.VirtualPortRange, + ) + if err != nil { + return fmt.Errorf( + "prüfen der virtuellen Ports fehlgeschlagen: %w", + err, + ) + } + + result := compareVirtualPorts( + configured, + available, + ) + + if len(result.Missing) == 0 && + len(result.Extra) == 0 { + return nil + } + + var message strings.Builder + + //////////////////////////////////////////////////////////////////////////// + // Missing ports + //////////////////////////////////////////////////////////////////////////// + + if len(result.Missing) > 0 && + len(result.Extra) == 0 { + + message.WriteString( + "Virtuelle Ports nicht eingerichtet", + ) + + message.WriteString( + "\n\nFolgende Ports sind in client.json " + + "eingetragen, aber auf diesem Rechner " + + "nicht eingerichtet:\n\n", + ) + + for _, port := range result.Missing { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nBitte richten Sie diese virtuellen Ports " + + "entsprechend der Installationsanleitung ein.", + ) + + return fmt.Errorf( + "%s", + strings.TrimSpace(message.String()), + ) + } + + //////////////////////////////////////////////////////////////////////////// + // Extra ports + //////////////////////////////////////////////////////////////////////////// + + if len(result.Missing) == 0 && + len(result.Extra) > 0 { + + message.WriteString( + "Virtuelle Ports nicht konfiguriert", + ) + + message.WriteString( + "\n\nFolgende virtuelle Ports sind auf diesem " + + "Rechner eingerichtet, aber nicht in " + + "client.json eingetragen:\n\n", + ) + + for _, port := range result.Extra { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nBitte ergänzen Sie die entsprechenden " + + "Einträge in client.json.", + ) + + return fmt.Errorf( + "%s", + strings.TrimSpace(message.String()), + ) + } + + //////////////////////////////////////////////////////////////////////////// + // Missing and extra ports + //////////////////////////////////////////////////////////////////////////// + + message.WriteString( + "Virtuelle Portkonfiguration stimmt nicht überein", + ) + + message.WriteString( + "\n\nIn client.json eingetragen, aber nicht " + + "eingerichtet:\n\n", + ) + + for _, port := range result.Missing { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nEingerichtet, aber nicht in client.json " + + "eingetragen:\n\n", + ) + + for _, port := range result.Extra { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nBitte korrigieren Sie die virtuelle " + + "Portkonfiguration.", + ) + + return fmt.Errorf( + "%s", + strings.TrimSpace(message.String()), + ) +} diff --git a/internal/client/virtual_port_validation_test.go b/internal/client/virtual_port_validation_test.go new file mode 100644 index 0000000..1e2afdd --- /dev/null +++ b/internal/client/virtual_port_validation_test.go @@ -0,0 +1,283 @@ +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_validation_test.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Tests für den Vergleich der konfigurierten und tatsächlich vorhandenen + * virtuellen rs2322tcp-Schnittstellen. + * ============================================================================ + */ +package client + +import ( + "reflect" + "testing" +) + +func TestCompareVirtualPortsIdentical(t *testing.T) { + configured := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + available := []string{ + "/dev/ttyUSB101", + "/dev/ttyUSB100", + } + + got := compareVirtualPorts( + configured, + available, + ) + + if len(got.Missing) != 0 { + t.Errorf( + "Missing = %v, want empty", + got.Missing, + ) + } + + if len(got.Extra) != 0 { + t.Errorf( + "Extra = %v, want empty", + got.Extra, + ) + } +} + +func TestCompareVirtualPortsMissing(t *testing.T) { + configured := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + "/dev/ttyUSB102", + } + + available := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + got := compareVirtualPorts( + configured, + available, + ) + + wantMissing := []string{ + "/dev/ttyUSB102", + } + + if !reflect.DeepEqual(got.Missing, wantMissing) { + t.Errorf( + "Missing = %v, want %v", + got.Missing, + wantMissing, + ) + } + + if len(got.Extra) != 0 { + t.Errorf( + "Extra = %v, want empty", + got.Extra, + ) + } +} + +func TestCompareVirtualPortsExtra(t *testing.T) { + configured := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + available := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + "/dev/ttyUSB103", + } + + got := compareVirtualPorts( + configured, + available, + ) + + if len(got.Missing) != 0 { + t.Errorf( + "Missing = %v, want empty", + got.Missing, + ) + } + + wantExtra := []string{ + "/dev/ttyUSB103", + } + + if !reflect.DeepEqual(got.Extra, wantExtra) { + t.Errorf( + "Extra = %v, want %v", + got.Extra, + wantExtra, + ) + } +} + +func TestCompareVirtualPortsMissingAndExtra(t *testing.T) { + configured := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + "/dev/ttyUSB102", + } + + available := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + "/dev/ttyUSB103", + } + + got := compareVirtualPorts( + configured, + available, + ) + + wantMissing := []string{ + "/dev/ttyUSB102", + } + + wantExtra := []string{ + "/dev/ttyUSB103", + } + + if !reflect.DeepEqual(got.Missing, wantMissing) { + t.Errorf( + "Missing = %v, want %v", + got.Missing, + wantMissing, + ) + } + + if !reflect.DeepEqual(got.Extra, wantExtra) { + t.Errorf( + "Extra = %v, want %v", + got.Extra, + wantExtra, + ) + } +} + +func TestCompareVirtualPortsEmpty(t *testing.T) { + got := compareVirtualPorts( + nil, + nil, + ) + + if len(got.Missing) != 0 { + t.Errorf( + "Missing = %v, want empty", + got.Missing, + ) + } + + if len(got.Extra) != 0 { + t.Errorf( + "Extra = %v, want empty", + got.Extra, + ) + } +} + +func TestCompareVirtualPortsConfiguredOnly(t *testing.T) { + configured := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + got := compareVirtualPorts( + configured, + nil, + ) + + wantMissing := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + if !reflect.DeepEqual(got.Missing, wantMissing) { + t.Errorf( + "Missing = %v, want %v", + got.Missing, + wantMissing, + ) + } + + if len(got.Extra) != 0 { + t.Errorf( + "Extra = %v, want empty", + got.Extra, + ) + } +} + +func TestCompareVirtualPortsAvailableOnly(t *testing.T) { + available := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + got := compareVirtualPorts( + nil, + available, + ) + + if len(got.Missing) != 0 { + t.Errorf( + "Missing = %v, want empty", + got.Missing, + ) + } + + wantExtra := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB101", + } + + if !reflect.DeepEqual(got.Extra, wantExtra) { + t.Errorf( + "Extra = %v, want %v", + got.Extra, + wantExtra, + ) + } +} + +func TestCompareVirtualPortsIgnoresDuplicates(t *testing.T) { + configured := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB100", + } + + available := []string{ + "/dev/ttyUSB100", + "/dev/ttyUSB100", + } + + got := compareVirtualPorts( + configured, + available, + ) + + if len(got.Missing) != 0 { + t.Errorf( + "Missing = %v, want empty", + got.Missing, + ) + } + + if len(got.Extra) != 0 { + t.Errorf( + "Extra = %v, want empty", + got.Extra, + ) + } +} diff --git a/internal/gui/app.go b/internal/gui/app.go index 0f78c87..73924a2 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -86,7 +86,7 @@ func NewApp( } window := fyneApp.NewWindow("rs2322tcp Client") - window.Resize(fyne.NewSize(600, 400)) + window.Resize(fyne.NewSize(800, 600)) app := &App{ fyneApp: fyneApp, @@ -164,10 +164,9 @@ func (a *App) Start() { a.statusLabel.SetText( "Server: nicht verbunden", ) - a.errorLabel.SetText( - fmt.Sprintf("Fehler: %v", err), - ) - a.reconnectButton.Enable() + a.errorLabel.SetText("") + + a.showStartupError(err) }) return @@ -209,10 +208,9 @@ func (a *App) reconnect() { a.statusLabel.SetText( "Server: nicht verbunden", ) - a.errorLabel.SetText( - fmt.Sprintf("Fehler: %v", err), - ) - a.reconnectButton.Enable() + a.errorLabel.SetText("") + + a.showStartupError(err) }) return @@ -234,6 +232,76 @@ func (a *App) reconnect() { }() } +/////////////////////////////////////////////////////////////////////////////// +// Startup error +/////////////////////////////////////////////////////////////////////////////// + +// showStartupError displays a modal error dialog for a client startup error. +// +// The dialog intentionally has only an "OK" button. A client configuration +// error must not be bypassed because starting the client with inconsistent +// virtual-port configuration could result in communication being directed +// to the wrong or missing local interface. +// +// After the user acknowledges the error, the technical client is closed and +// the complete Fyne application is terminated. +func (a *App) showStartupError(err error) { + if a == nil || a.window == nil || err == nil { + return + } + + title := widget.NewLabelWithStyle( + "Die virtuellen Ports konnten nicht geprüft werden.", + fyne.TextAlignLeading, + fyne.TextStyle{ + Bold: true, + }, + ) + + message := widget.NewLabel( + err.Error(), + ) + + message.Wrapping = fyne.TextWrapWord + + content := container.NewVBox( + title, + widget.NewSeparator(), + message, + ) + + confirm := dialog.NewCustomWithoutButtons( + "Virtuelle Ports", + container.NewPadded(content), + a.window, + ) + + okButton := widget.NewButton( + "OK", + func() { + confirm.Dismiss() + + _ = a.clientApplication.Close() + + a.window.SetCloseIntercept(nil) + + a.fyneApp.Quit() + }, + ) + + confirm.SetButtons( + []fyne.CanvasObject{ + okButton, + }, + ) + + confirm.Show() + + confirm.Resize( + fyne.NewSize(650, 420), + ) +} + /////////////////////////////////////////////////////////////////////////////// // Device assignment /////////////////////////////////////////////////////////////////////////////// @@ -398,6 +466,7 @@ func (a *App) refreshAssignmentWindow( ) }, ) + footer := container.NewVBox( info, closeButton,