rs2322tcp/internal/client/application.go
2026-08-11 12:51:17 +02:00

297 lines
7.1 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
* 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, connects to the server and starts
// the client Runtime.
//
// Start is intended for the initial application startup. An already running
// application must be stopped first by calling Close or Reconnect.
//
// The Runtime runs asynchronously because it waits for the configured
// bridges while the GUI must remain responsive.
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)
}
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 client 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
}
// 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
}
///////////////////////////////////////////////////////////////////////////////
// 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
}