Linux-Portverwaltung für Windows-Erweiterung korrigieren

This commit is contained in:
Dieter Lang 2026-08-21 12:20:49 +02:00
parent a6cd04ece4
commit 61bd03b863
4 changed files with 66 additions and 249 deletions

View file

@ -11,6 +11,11 @@
{ {
"port": "COM100", "port": "COM100",
"remote_device": "rotor" "remote_device": "rotor"
},
{
"port": "COM101",
"remote_device": "radio"
} }
] ]
} }

View file

@ -17,14 +17,13 @@
* Link unter ~/.rs2322tcp/virtual/. * Link unter ~/.rs2322tcp/virtual/.
* ============================================================================ * ============================================================================
*/ */
package client package client
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"git.lang-dieter.de/rs2322tcp/internal/config"
) )
const ( const (
@ -77,6 +76,9 @@ func ensureVirtualPortDirectory() error {
// //
// The link itself is located below ~/.rs2322tcp/virtual/ and points to the // The link itself is located below ~/.rs2322tcp/virtual/ and points to the
// current PTY, for example /dev/pts/2. // 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 { func setVirtualPortLink(portPath string, target string) error {
if portPath == "" { if portPath == "" {
return fmt.Errorf("virtual port path is empty") return fmt.Errorf("virtual port path is empty")
@ -95,8 +97,10 @@ func setVirtualPortLink(portPath string, target string) error {
return err return err
} }
// Remove an existing link. The installation-owned outer /dev/ttyUSBxxx // Replace only the user-owned internal link.
// link is deliberately never touched here. //
// The installation-owned /dev/ttyUSBxxx link is deliberately never
// touched here.
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) { if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf( return fmt.Errorf(
"remove existing virtual port link %q: %w", "remove existing virtual port link %q: %w",
@ -117,9 +121,12 @@ func setVirtualPortLink(portPath string, target string) error {
return nil 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 { func removeVirtualPortLink(portPath string) error {
if portPath == "" { if portPath == "" {
return fmt.Errorf("virtual port path is empty") return fmt.Errorf("virtual port path is empty")
@ -140,73 +147,3 @@ func removeVirtualPortLink(portPath string) error {
return nil 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
}

View file

@ -9,186 +9,39 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* Beschreibung: * 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 * Die gemeinsame Verwaltung von ManagedVirtualPort und VirtualPortManager
* bereitgestellt. Der Client verwaltet ausschließlich den internen Link * befindet sich in virtual_port_manager_open.go.
* unter ~/.rs2322tcp/virtual/.
* *
* Virtuelle Ports können entweder automatisch über den nächsten freien Port * Unter Linux wird der PTY über newVirtualSerial() erzeugt und der interne
* oder gezielt über eine vorgegebene Portnummer geöffnet werden. * Link unter ~/.rs2322tcp/virtual/ auf diesen PTY gesetzt.
* ============================================================================ * ============================================================================
*/ */
package client package client
import ( import (
"fmt" "fmt"
"sync"
) )
/////////////////////////////////////////////////////////////////////////////// // openReserved creates the actual Linux virtual serial port for an already
// 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. // reserved port number.
// //
// The caller must reserve the port before calling this method. // 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) portPath := m.PortPath(number)
serial, err := newVirtualSerial() serial, err := newVirtualSerial()

View file

@ -9,7 +9,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* Beschreibung: * 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 package client
@ -71,10 +72,20 @@ func TestVirtualPortManagerOpen(t *testing.T) {
t.Fatalf("Close() failed: %v", err) t.Fatalf("Close() failed: %v", err)
} }
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) { targetAfterClose, err := os.Readlink(linkPath)
if err != nil {
t.Fatalf( t.Fatalf(
"internal link still exists after Close(): %q", "internal link does not exist after Close(): %q: %v",
linkPath, 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) t.Fatalf("Close() failed: %v", err)
} }
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) { targetAfterClose, err := os.Readlink(linkPath)
if err != nil {
t.Fatalf( t.Fatalf(
"internal link still exists after Close(): %q", "internal link does not exist after Close(): %q: %v",
linkPath, linkPath,
err,
)
}
if targetAfterClose != target {
t.Errorf(
"internal link target after Close() = %q, want %q",
targetAfterClose,
target,
) )
} }
} }
func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) { func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) {
tempHome := t.TempDir() tempHome := t.TempDir()
t.Setenv("HOME", tempHome) t.Setenv("HOME", tempHome)