rs2322tcp/internal/server/data_connection.go
2026-08-10 10:38:14 +02:00

169 lines
3.5 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: data_connection.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Bidirektionale Datenverbindung zwischen einer TCP-Verbindung und
* einem seriellen Gerätekanal.
* ============================================================================
*/
package server
import (
"fmt"
"io"
"net"
"sync"
)
///////////////////////////////////////////////////////////////////////////////
// DataConnection
///////////////////////////////////////////////////////////////////////////////
// DataConnection connects one TCP connection with one serial device.
//
// Data is transferred bidirectionally:
//
// TCP -> Serial
// TCP <- Serial
//
// The serial side is represented by an io.ReadWriteCloser so that this
// server component does not depend directly on the concrete serial
// implementation.
type DataConnection struct {
tcp net.Conn
serial io.ReadWriteCloser
closeOnce sync.Once
closeErr error
}
///////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////
// NewDataConnection creates a new bidirectional data connection.
func NewDataConnection(
tcp net.Conn,
serial io.ReadWriteCloser,
) (*DataConnection, error) {
if tcp == nil {
return nil, fmt.Errorf("TCP connection is nil")
}
if serial == nil {
return nil, fmt.Errorf("serial connection is nil")
}
return &DataConnection{
tcp: tcp,
serial: serial,
}, nil
}
///////////////////////////////////////////////////////////////////////////////
// Properties
///////////////////////////////////////////////////////////////////////////////
// TCPConn returns the TCP connection.
func (c *DataConnection) TCPConn() net.Conn {
if c == nil {
return nil
}
return c.tcp
}
// SerialConn returns the serial connection.
func (c *DataConnection) SerialConn() io.ReadWriteCloser {
if c == nil {
return nil
}
return c.serial
}
///////////////////////////////////////////////////////////////////////////////
// Run
///////////////////////////////////////////////////////////////////////////////
// Run transfers data in both directions.
//
// Run blocks until one of the two transfer directions terminates.
// The other direction is then stopped and both connections are closed.
//
// The first non-EOF transfer error is returned.
func (c *DataConnection) Run() error {
if c == nil {
return fmt.Errorf("data connection is nil")
}
var wg sync.WaitGroup
errCh := make(chan error, 2)
wg.Add(2)
go func() {
defer wg.Done()
_, err := io.Copy(c.serial, c.tcp)
errCh <- err
}()
go func() {
defer wg.Done()
_, err := io.Copy(c.tcp, c.serial)
errCh <- err
}()
err := <-errCh
_ = c.Close()
wg.Wait()
if err == nil || err == io.EOF {
return nil
}
return err
}
///////////////////////////////////////////////////////////////////////////////
// Lifecycle
///////////////////////////////////////////////////////////////////////////////
// Close closes both sides of the data connection.
//
// Close is safe to call multiple times.
func (c *DataConnection) Close() error {
if c == nil {
return nil
}
c.closeOnce.Do(func() {
var firstErr error
if c.tcp != nil {
if err := c.tcp.Close(); err != nil {
firstErr = err
}
}
if c.serial != nil {
if err := c.serial.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
c.closeErr = firstErr
})
return c.closeErr
}