rs2322tcp/internal/client/virtual_port_manager_linux.go
2026-08-11 11:29:49 +02:00

227 lines
5.3 KiB
Go

//go:build linux
/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/client/virtual_port_manager_linux.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Linux-spezifische Erzeugung und Verwaltung virtueller rs2322tcp-Ports.
*
* Der öffentliche /dev/ttyUSBxxx-Eintrag wird bei der Installation
* bereitgestellt. Der Client verwaltet ausschließlich den internen Link
* unter ~/.rs2322tcp/virtual/.
*
* Virtuelle Ports können entweder automatisch über den nächsten freien Port
* oder gezielt über eine vorgegebene Portnummer geöffnet werden.
* ============================================================================
*/
package client
import (
"fmt"
"sync"
)
///////////////////////////////////////////////////////////////////////////////
// Managed virtual port
///////////////////////////////////////////////////////////////////////////////
// ManagedVirtualPort verbindet einen reservierten Port mit einer
// VirtualSerial-Instanz und verwaltet deren Lebenszyklus.
//
// Der Mutex schützt den internen Port-Pointer gegen gleichzeitige Zugriffe
// aus Read, Write und Close.
type ManagedVirtualPort struct {
mu sync.Mutex
port *VirtualPort
manager *VirtualPortManager
number int
}
// Path returns the device path visible to the external application.
func (p *ManagedVirtualPort) Path() string {
if p == nil {
return ""
}
p.mu.Lock()
defer p.mu.Unlock()
if p.port == nil {
return ""
}
return p.port.Path()
}
// Read reads data from the underlying virtual serial device.
func (p *ManagedVirtualPort) Read(b []byte) (int, error) {
if p == nil {
return 0, fmt.Errorf("managed virtual port is nil")
}
p.mu.Lock()
port := p.port
p.mu.Unlock()
if port == nil {
return 0, fmt.Errorf("managed virtual port is closed")
}
return port.Read(b)
}
// Write writes data to the underlying virtual serial device.
func (p *ManagedVirtualPort) Write(b []byte) (int, error) {
if p == nil {
return 0, fmt.Errorf("managed virtual port is nil")
}
p.mu.Lock()
port := p.port
p.mu.Unlock()
if port == nil {
return 0, fmt.Errorf("managed virtual port is closed")
}
return port.Write(b)
}
// Close removes the internal link, closes the PTY and releases the
// reservation.
//
// The managed port is detached from the object before the underlying PTY
// is closed. This prevents a concurrent Read or Write from accessing the
// managed port pointer after Close has taken ownership of it.
func (p *ManagedVirtualPort) Close() error {
if p == nil {
return nil
}
p.mu.Lock()
port := p.port
p.port = nil
manager := p.manager
number := p.number
p.mu.Unlock()
if port == nil {
return nil
}
var firstErr error
if err := removeVirtualPortLink(port.Path()); err != nil {
firstErr = err
}
if err := port.Close(); err != nil && firstErr == nil {
firstErr = err
}
if manager != nil {
manager.Release(number)
}
return firstErr
}
///////////////////////////////////////////////////////////////////////////////
// Open
///////////////////////////////////////////////////////////////////////////////
// Open creates a new virtual serial port using the next free port number.
//
// The internal PTY is created by newVirtualSerial(). The internal
// ~/.rs2322tcp/virtual/ttyUSBxxx link is then pointed at the PTY.
//
// Open retains the automatic allocation behavior used by the existing
// client code.
func (m *VirtualPortManager) Open() (*ManagedVirtualPort, error) {
if m == nil {
return nil, fmt.Errorf("virtual port manager is nil")
}
number, err := m.Reserve()
if err != nil {
return nil, err
}
return m.openReserved(number)
}
// OpenSpecific creates a new virtual serial port using exactly the specified
// port number.
//
// Unlike Open, this method does not select the next free port. The requested
// number is reserved explicitly and is therefore suitable for the
// VirtualPortConfig.Port value loaded from client.json.
//
// If creation of the PTY or the internal link fails, the reservation is
// released again.
func (m *VirtualPortManager) OpenSpecific(number int) (*ManagedVirtualPort, error) {
if m == nil {
return nil, fmt.Errorf("virtual port manager is nil")
}
if err := m.ReserveSpecific(number); err != nil {
return nil, err
}
return m.openReserved(number)
}
///////////////////////////////////////////////////////////////////////////////
// Internal open helper
///////////////////////////////////////////////////////////////////////////////
// openReserved creates the actual virtual serial port for an already
// reserved port number.
//
// The caller must reserve the port before calling this method.
func (m *VirtualPortManager) openReserved(number int) (*ManagedVirtualPort, error) {
portPath := m.PortPath(number)
serial, err := newVirtualSerial()
if err != nil {
m.Release(number)
return nil, fmt.Errorf(
"create virtual serial for %s: %w",
portPath,
err,
)
}
if err := setVirtualPortLink(
portPath,
serial.Path(),
); err != nil {
_ = serial.Close()
m.Release(number)
return nil, fmt.Errorf(
"bind %s to %s: %w",
portPath,
serial.Path(),
err,
)
}
port := NewVirtualPort(portPath, serial)
return &ManagedVirtualPort{
port: port,
manager: m,
number: number,
}, nil
}