//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/. * ============================================================================ */ 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. 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 } 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 }