347 lines
8.3 KiB
Go
347 lines
8.3 KiB
Go
/*
|
|
* ============================================================================
|
|
* Projekt.....: rs2322tcp
|
|
* Datei.......: internal/client/application.go
|
|
* Copyright (C) 2026 Dieter Lang
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* Beschreibung:
|
|
* Gemeinsame Anwendungsschicht für den rs2322tcp-Client.
|
|
*
|
|
* Die Anwendungsschicht verbindet die Konfiguration, die Control-Verbindung
|
|
* zum Server und die clientseitige Runtime. Sie stellt damit einen einfachen
|
|
* Lebenszyklus für die spätere grafische Benutzeroberfläche bereit.
|
|
*
|
|
* Der technische Client wird bei Start und Reconnect vollständig neu
|
|
* initialisiert:
|
|
*
|
|
* Konfiguration laden
|
|
* |
|
|
* v
|
|
* Lokale virtuelle Ports prüfen
|
|
* |
|
|
* v
|
|
* Server verbinden
|
|
* |
|
|
* v
|
|
* Geräteliste abfragen
|
|
* |
|
|
* v
|
|
* Runtime starten
|
|
*
|
|
* Ein Reconnect beendet zunächst die bestehende Runtime und Control-
|
|
* Verbindung und führt anschließend denselben Startablauf erneut aus.
|
|
*
|
|
* Die Anwendungsschicht enthält bewusst keine GUI-Logik und keine
|
|
* Fyne-Abhängigkeit.
|
|
* ============================================================================
|
|
*/
|
|
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
|
"git.lang-dieter.de/rs2322tcp/internal/transport"
|
|
)
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Application
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Application represents one running rs2322tcp client application.
|
|
//
|
|
// Application owns the current client connection and Runtime. The
|
|
// configuration file is the persistent source of the client configuration.
|
|
//
|
|
// The Application is intended to be used by the command-line entry point
|
|
// as well as by the later graphical user interface.
|
|
type Application struct {
|
|
mu sync.Mutex
|
|
|
|
configFile string
|
|
|
|
config *config.ClientConfig
|
|
client *Client
|
|
runtime *Runtime
|
|
devices []transport.RemoteDeviceInfo
|
|
|
|
runtimeDone chan error
|
|
}
|
|
|
|
// NewApplication creates a new client Application.
|
|
//
|
|
// The client configuration is loaded immediately. No network connection
|
|
// is established. Start must be called before the application connects to
|
|
// the server or starts the Runtime.
|
|
func NewApplication(configFile string) (*Application, error) {
|
|
if configFile == "" {
|
|
return nil, fmt.Errorf("configuration file is empty")
|
|
}
|
|
|
|
cfg, err := config.LoadClient(configFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load client configuration: %w", err)
|
|
}
|
|
|
|
return &Application{
|
|
configFile: configFile,
|
|
config: cfg,
|
|
}, nil
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Start
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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.
|
|
func (a *Application) Start() error {
|
|
if a == nil {
|
|
return fmt.Errorf("application is nil")
|
|
}
|
|
|
|
a.mu.Lock()
|
|
|
|
if a.runtime != nil || a.client != nil {
|
|
a.mu.Unlock()
|
|
|
|
return fmt.Errorf("application is already running")
|
|
}
|
|
|
|
a.mu.Unlock()
|
|
|
|
cfg, err := config.LoadClient(a.configFile)
|
|
if err != nil {
|
|
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,
|
|
cfg.Server.Port,
|
|
)
|
|
|
|
client, err := New(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
devices, err := client.GetDevices()
|
|
if err != nil {
|
|
_ = client.Close()
|
|
|
|
return fmt.Errorf("get remote devices: %w", err)
|
|
}
|
|
|
|
manager := NewVirtualPortManager(
|
|
cfg.VirtualPortRange,
|
|
)
|
|
|
|
runtime, err := NewRuntime(
|
|
client,
|
|
manager,
|
|
)
|
|
if err != nil {
|
|
_ = client.Close()
|
|
|
|
return fmt.Errorf("create runtime: %w", err)
|
|
}
|
|
|
|
runtimeDone := make(chan error, 1)
|
|
|
|
a.mu.Lock()
|
|
|
|
a.config = cfg
|
|
a.client = client
|
|
a.runtime = runtime
|
|
a.devices = append(
|
|
[]transport.RemoteDeviceInfo(nil),
|
|
devices...,
|
|
)
|
|
a.runtimeDone = runtimeDone
|
|
|
|
a.mu.Unlock()
|
|
|
|
go func() {
|
|
runtimeDone <- runtime.Run(*cfg)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Reconnect
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Reconnect stops the current client connection and starts it again.
|
|
//
|
|
// The configuration file is loaded again during Start. This is intentional:
|
|
// changes made by the GUI are therefore picked up automatically.
|
|
//
|
|
// Reconnect is also useful when the server connection has been lost or the
|
|
// user explicitly requests a new connection.
|
|
func (a *Application) Reconnect() error {
|
|
if a == nil {
|
|
return fmt.Errorf("application is nil")
|
|
}
|
|
|
|
if err := a.Close(); err != nil {
|
|
return fmt.Errorf("close current connection: %w", err)
|
|
}
|
|
|
|
return a.Start()
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Close
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Close stops the Runtime and closes the client control connection.
|
|
//
|
|
// Calling Close more than once is safe.
|
|
func (a *Application) Close() error {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
|
|
a.mu.Lock()
|
|
|
|
runtime := a.runtime
|
|
client := a.client
|
|
|
|
a.runtime = nil
|
|
a.client = nil
|
|
a.devices = nil
|
|
a.runtimeDone = nil
|
|
|
|
a.mu.Unlock()
|
|
|
|
var firstErr error
|
|
|
|
if runtime != nil {
|
|
if err := runtime.Close(); err != nil {
|
|
firstErr = err
|
|
}
|
|
} else if client != nil {
|
|
if err := client.Close(); err != nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
|
|
return firstErr
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Configuration
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Config returns a copy of the currently loaded client configuration.
|
|
//
|
|
// The configuration is available after NewApplication, even when the
|
|
// server connection has not yet been established.
|
|
func (a *Application) Config() *config.ClientConfig {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
if a.config == nil {
|
|
return nil
|
|
}
|
|
|
|
cfg := *a.config
|
|
|
|
cfg.VirtualPorts = append(
|
|
[]config.VirtualPortConfig(nil),
|
|
a.config.VirtualPorts...,
|
|
)
|
|
|
|
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
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Devices returns a copy of the remote devices received from the server.
|
|
//
|
|
// The returned slice belongs to the caller and can therefore be modified
|
|
// without changing the Application state.
|
|
func (a *Application) Devices() []transport.RemoteDeviceInfo {
|
|
if a == nil {
|
|
return nil
|
|
}
|
|
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
return append(
|
|
[]transport.RemoteDeviceInfo(nil),
|
|
a.devices...,
|
|
)
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Runtime state
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Running reports whether the application currently owns a Runtime and a
|
|
// client connection.
|
|
func (a *Application) Running() bool {
|
|
if a == nil {
|
|
return false
|
|
}
|
|
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
|
|
return a.runtime != nil && a.client != nil
|
|
}
|