rs2322tcp/internal/client/runtime.go
2026-08-10 17:59:35 +02:00

216 lines
4.9 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/client/runtime.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Clientseitige Laufzeitverwaltung für die konfigurierten virtuellen
* seriellen Schnittstellen.
*
* Die Runtime verbindet die in der Client-Konfiguration angegebenen
* virtuellen Ports mit den zugehörigen entfernten Geräten.
*
* Für jedes konfigurierte virtuelle Gerät wird eine eigene Bridge gestartet:
*
* /dev/ttyUSBxxx ─── Bridge ─── TCP Data Connection
*
* Die Control-Verbindung zum Server wird gemeinsam von allen Bridges
* verwendet.
* ============================================================================
*/
package client
import (
"fmt"
"sync"
"git.lang-dieter.de/rs2322tcp/internal/config"
)
///////////////////////////////////////////////////////////////////////////////
// Runtime
///////////////////////////////////////////////////////////////////////////////
// Runtime manages the client-side virtual serial connections.
//
// A Runtime owns the Client control connection and the VirtualPortManager.
// Each configured virtual port is represented by one running Bridge.
type Runtime struct {
client *Client
manager *VirtualPortManager
mu sync.Mutex
bridges []*Bridge
}
// NewRuntime creates a new client Runtime.
//
// The Runtime does not establish a server connection and does not open any
// virtual ports until Run is called.
func NewRuntime(
client *Client,
manager *VirtualPortManager,
) (*Runtime, error) {
if client == nil {
return nil, fmt.Errorf("client is nil")
}
if manager == nil {
return nil, fmt.Errorf("virtual port manager is nil")
}
return &Runtime{
client: client,
manager: manager,
}, nil
}
///////////////////////////////////////////////////////////////////////////////
// Run
///////////////////////////////////////////////////////////////////////////////
// Run starts all configured virtual serial connections.
//
// The supplied configuration determines which local virtual ports are
// assigned to which remote devices.
//
// Run blocks until one bridge terminates or an error occurs while creating
// one of the configured bridges.
func (r *Runtime) Run(
cfg config.ClientConfig,
) error {
if r == nil || r.client == nil {
return fmt.Errorf("runtime is not initialized")
}
if r.manager == nil {
return fmt.Errorf("virtual port manager is nil")
}
if len(cfg.VirtualPorts) == 0 {
return nil
}
devices, err := r.client.GetDevices()
if err != nil {
return fmt.Errorf("get remote devices: %w", err)
}
for _, virtualPortConfig := range cfg.VirtualPorts {
bridge, _, _, err := connectBridge(
r.client,
r.manager,
virtualPortConfig,
devices,
)
if err != nil {
r.Close()
return err
}
r.mu.Lock()
r.bridges = append(r.bridges, bridge)
r.mu.Unlock()
}
return r.waitForBridge()
}
///////////////////////////////////////////////////////////////////////////////
// Bridge lifecycle
///////////////////////////////////////////////////////////////////////////////
// waitForBridge waits until one of the running bridges terminates.
//
// A bridge terminating is considered the end of the Runtime. The Bridge
// itself closes its serial and TCP endpoints when Run returns.
func (r *Runtime) waitForBridge() error {
results := make(chan error, len(r.bridges))
r.mu.Lock()
bridges := append([]*Bridge(nil), r.bridges...)
r.mu.Unlock()
for _, bridge := range bridges {
go func(b *Bridge) {
results <- b.Run()
}(bridge)
}
err := <-results
r.Close()
return err
}
///////////////////////////////////////////////////////////////////////////////
// Close
///////////////////////////////////////////////////////////////////////////////
// Close closes all resources owned by the Runtime.
//
// Closing the bridge endpoints causes running Bridge.Run calls to terminate.
// The client control connection is closed afterwards.
//
// Calling Close more than once is safe.
func (r *Runtime) Close() error {
if r == nil {
return nil
}
r.mu.Lock()
bridges := append([]*Bridge(nil), r.bridges...)
r.bridges = nil
r.mu.Unlock()
var firstErr error
for _, bridge := range bridges {
if bridge == nil {
continue
}
if bridge.serial != nil {
if err := bridge.serial.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
if bridge.conn != nil {
if err := bridge.conn.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
}
if r.client != nil {
if err := r.client.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
// Client returns the Client used by the Runtime.
func (r *Runtime) Client() *Client {
if r == nil {
return nil
}
return r.client
}
// Manager returns the VirtualPortManager used by the Runtime.
func (r *Runtime) Manager() *VirtualPortManager {
if r == nil {
return nil
}
return r.manager
}