diff --git a/configs/client.json b/configs/client.json index d8f17ac..0ab54c6 100644 --- a/configs/client.json +++ b/configs/client.json @@ -11,6 +11,11 @@ { "port": "COM100", "remote_device": "rotor" + }, + { + "port": "COM101", + "remote_device": "radio" } + ] -} \ No newline at end of file +} diff --git a/internal/client/virtual_port_linux.go b/internal/client/virtual_port_linux.go index 42e820d..0cc0284 100644 --- a/internal/client/virtual_port_linux.go +++ b/internal/client/virtual_port_linux.go @@ -17,14 +17,13 @@ * Link unter ~/.rs2322tcp/virtual/. * ============================================================================ */ + package client import ( "fmt" "os" "path/filepath" - - "git.lang-dieter.de/rs2322tcp/internal/config" ) const ( @@ -77,6 +76,9 @@ func ensureVirtualPortDirectory() error { // // The link itself is located below ~/.rs2322tcp/virtual/ and points to the // current PTY, for example /dev/pts/2. +// +// The public /dev/ttyUSBxxx installation link is deliberately never +// modified here. func setVirtualPortLink(portPath string, target string) error { if portPath == "" { return fmt.Errorf("virtual port path is empty") @@ -95,8 +97,10 @@ func setVirtualPortLink(portPath string, target string) error { return err } - // Remove an existing link. The installation-owned outer /dev/ttyUSBxxx - // link is deliberately never touched here. + // Replace only the user-owned internal link. + // + // The installation-owned /dev/ttyUSBxxx link is deliberately never + // touched here. if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf( "remove existing virtual port link %q: %w", @@ -117,9 +121,12 @@ func setVirtualPortLink(portPath string, target string) error { return nil } -// removeVirtualPortLink removes the internal link for a virtual port. +// removeVirtualPortLink removes the user-owned internal link for a virtual +// port. // -// The public /dev/ttyUSBxxx link is never modified. +// This function is retained for explicit link-management operations and +// tests. It is NOT called by ManagedVirtualPort.Close(), because the +// installation-owned symlink chain must remain available. func removeVirtualPortLink(portPath string) error { if portPath == "" { return fmt.Errorf("virtual port path is empty") @@ -140,73 +147,3 @@ func removeVirtualPortLink(portPath string) error { return nil } - -// existingVirtualPortLinks returns all public virtual-port paths that -// currently exist as symbolic links. -// -// Only ports inside the configured virtual-port range are considered. -// The public /dev/ttyUSBxxx links are only inspected; they are never -// created, changed, or removed by this function. -// -// This check only requires read access and therefore does not require -// root privileges. -func existingVirtualPortLinks( - cfg config.VirtualPortRangeConfig, -) ([]string, error) { - return existingVirtualPortLinksInDirectory( - cfg, - "/dev", - ) -} - -// existingVirtualPortLinksInDirectory returns all virtual-port paths in -// directory that currently exist as symbolic links. -// -// This helper is deliberately separated from existingVirtualPortLinks so -// the filesystem inspection can be tested without modifying /dev. -func existingVirtualPortLinksInDirectory( - cfg config.VirtualPortRangeConfig, - directory string, -) ([]string, error) { - if directory == "" { - return nil, fmt.Errorf("virtual port directory is empty") - } - - manager := NewVirtualPortManager(cfg) - if manager == nil { - return nil, fmt.Errorf("virtual port manager is nil") - } - - links := make([]string, 0) - - for number := cfg.First; number <= cfg.Last; number++ { - portName := filepath.Base( - manager.PortPath(number), - ) - portPath := filepath.Join(directory, portName) - - info, err := os.Lstat(portPath) - if err != nil { - if os.IsNotExist(err) { - continue - } - - return nil, fmt.Errorf( - "inspect virtual port %q: %w", - portPath, - err, - ) - } - - if info.Mode()&os.ModeSymlink == 0 { - continue - } - - links = append( - links, - filepath.Join(directory, portName), - ) - } - - return links, nil -} diff --git a/internal/client/virtual_port_manager_linux.go b/internal/client/virtual_port_manager_linux.go index d190ca1..46cc89c 100644 --- a/internal/client/virtual_port_manager_linux.go +++ b/internal/client/virtual_port_manager_linux.go @@ -9,186 +9,39 @@ * SPDX-License-Identifier: GPL-3.0-or-later * * Beschreibung: - * Linux-spezifische Erzeugung und Verwaltung virtueller rs2322tcp-Ports. + * Linux-spezifische Erzeugung eines virtuellen rs2322tcp-Ports. * - * Der öffentliche /dev/ttyUSBxxx-Eintrag wird bei der Installation - * bereitgestellt. Der Client verwaltet ausschließlich den internen Link - * unter ~/.rs2322tcp/virtual/. + * Die gemeinsame Verwaltung von ManagedVirtualPort und VirtualPortManager + * befindet sich in virtual_port_manager_open.go. * - * Virtuelle Ports können entweder automatisch über den nächsten freien Port - * oder gezielt über eine vorgegebene Portnummer geöffnet werden. + * Unter Linux wird der PTY über newVirtualSerial() erzeugt und der interne + * Link unter ~/.rs2322tcp/virtual/ auf diesen PTY gesetzt. * ============================================================================ */ + 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 +// openReserved creates the actual Linux 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) { +// +// The public /dev/ttyUSBxxx link is not modified here. The installation-owned +// link remains untouched. Only the user-owned internal link below +// ~/.rs2322tcp/virtual/ is created or updated to point to the newly created +// PTY. +func openReserved( + m *VirtualPortManager, + number int, +) (*ManagedVirtualPort, error) { + if m == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + portPath := m.PortPath(number) serial, err := newVirtualSerial() diff --git a/internal/client/virtual_port_manager_linux_test.go b/internal/client/virtual_port_manager_linux_test.go index 810ea9a..c678ab7 100644 --- a/internal/client/virtual_port_manager_linux_test.go +++ b/internal/client/virtual_port_manager_linux_test.go @@ -9,7 +9,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later * * Beschreibung: - * Linux-spezifische Tests für die Erzeugung virtueller rs2322tcp-Ports. + * Linux-spezifische Tests für die Erzeugung und Verwaltung virtueller + * rs2322tcp-Ports. * ============================================================================ */ package client @@ -71,10 +72,20 @@ func TestVirtualPortManagerOpen(t *testing.T) { t.Fatalf("Close() failed: %v", err) } - if _, err := os.Lstat(linkPath); !os.IsNotExist(err) { + targetAfterClose, err := os.Readlink(linkPath) + if err != nil { t.Fatalf( - "internal link still exists after Close(): %q", + "internal link does not exist after Close(): %q: %v", linkPath, + err, + ) + } + + if targetAfterClose != target { + t.Errorf( + "internal link target after Close() = %q, want %q", + targetAfterClose, + target, ) } } @@ -125,13 +136,24 @@ func TestVirtualPortManagerOpenSpecific(t *testing.T) { t.Fatalf("Close() failed: %v", err) } - if _, err := os.Lstat(linkPath); !os.IsNotExist(err) { + targetAfterClose, err := os.Readlink(linkPath) + if err != nil { t.Fatalf( - "internal link still exists after Close(): %q", + "internal link does not exist after Close(): %q: %v", linkPath, + err, + ) + } + + if targetAfterClose != target { + t.Errorf( + "internal link target after Close() = %q, want %q", + targetAfterClose, + target, ) } } + func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) { tempHome := t.TempDir() t.Setenv("HOME", tempHome)