/* * ============================================================================ * 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 { if virtualPortConfig.RemoteDevice == "" { continue } 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 }