Respect configured virtual port assignments
This commit is contained in:
parent
f3610b989c
commit
80e4196e16
6 changed files with 371 additions and 4 deletions
|
|
@ -9,6 +9,14 @@
|
|||
* Beschreibung:
|
||||
* Aufbau einer Verbindung zwischen einem lokalen virtuellen seriellen Port
|
||||
* und dem zugehörigen entfernten Gerät.
|
||||
*
|
||||
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
|
||||
* übernommen. Eine Konfiguration wie
|
||||
*
|
||||
* /dev/ttyUSB100 -> radio
|
||||
*
|
||||
* führt somit technisch auch zur Erzeugung beziehungsweise Verwendung von
|
||||
* /dev/ttyUSB100 und nicht einfach zum nächsten freien Port.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
|
@ -16,19 +24,65 @@ package client
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
"git.lang-dieter.de/rs2322tcp/internal/transport"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Virtual port configuration
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// virtualPortNumber converts a configured /dev/ttyUSBxxx path into its
|
||||
// numeric virtual port number.
|
||||
//
|
||||
// The client configuration stores the complete Linux device path, while
|
||||
// VirtualPortManager works with the numeric port number. Only the exact
|
||||
// /dev/ttyUSB<number> format is accepted here.
|
||||
func virtualPortNumber(path string) (int, error) {
|
||||
const prefix = "/dev/ttyUSB"
|
||||
|
||||
if !strings.HasPrefix(path, prefix) {
|
||||
return 0, fmt.Errorf(
|
||||
"invalid virtual port %q: expected /dev/ttyUSB<number>",
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
value := strings.TrimPrefix(path, prefix)
|
||||
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf(
|
||||
"invalid virtual port %q: port number is missing",
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
number, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(
|
||||
"invalid virtual port %q: invalid port number",
|
||||
path,
|
||||
)
|
||||
}
|
||||
|
||||
return number, nil
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Virtual port connection
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// connectVirtualPort opens the local virtual port and the corresponding
|
||||
// remote TCP data connection and returns both endpoints.
|
||||
// connectVirtualPort opens the configured local virtual port and the
|
||||
// corresponding remote TCP data connection and returns both endpoints.
|
||||
//
|
||||
// The caller owns the returned virtual port and TCP connection.
|
||||
//
|
||||
// The local virtual port is opened using exactly the port number specified
|
||||
// in virtualPortConfig.Port. This preserves the user's configured mapping
|
||||
// between local virtual interfaces and remote devices.
|
||||
func connectVirtualPort(
|
||||
c *Client,
|
||||
manager *VirtualPortManager,
|
||||
|
|
@ -48,10 +102,16 @@ func connectVirtualPort(
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
virtualPort, err := manager.Open()
|
||||
portNumber, err := virtualPortNumber(virtualPortConfig.Port)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
virtualPort, err := manager.OpenSpecific(portNumber)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf(
|
||||
"open virtual port for %q: %w",
|
||||
"open virtual port %q for %q: %w",
|
||||
virtualPortConfig.Port,
|
||||
virtualPortConfig.RemoteDevice,
|
||||
err,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -367,6 +367,83 @@ func TestConnectVirtualPortSuccess(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) {
|
||||
dataAddress, accepted := startTestDataServer(t)
|
||||
|
||||
device := remoteDeviceFromAddress(
|
||||
t,
|
||||
"radio",
|
||||
dataAddress,
|
||||
)
|
||||
|
||||
client := newTestClient(t)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
},
|
||||
)
|
||||
|
||||
cfg := config.VirtualPortConfig{
|
||||
Port: "/dev/ttyUSB150",
|
||||
RemoteDevice: "radio",
|
||||
}
|
||||
|
||||
virtualPort, dataConn, err := connectVirtualPort(
|
||||
client,
|
||||
manager,
|
||||
cfg,
|
||||
[]transport.RemoteDeviceInfo{device},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("connectVirtualPort: %v", err)
|
||||
}
|
||||
|
||||
if virtualPort == nil {
|
||||
t.Fatal("virtual port is nil")
|
||||
}
|
||||
|
||||
if dataConn == nil {
|
||||
t.Fatal("data connection is nil")
|
||||
}
|
||||
|
||||
defer virtualPort.Close()
|
||||
defer dataConn.Close()
|
||||
|
||||
if virtualPort.Path() != "/dev/ttyUSB150" {
|
||||
t.Fatalf(
|
||||
"virtual port path = %q, want %q",
|
||||
virtualPort.Path(),
|
||||
"/dev/ttyUSB150",
|
||||
)
|
||||
}
|
||||
|
||||
// Port 100 must still be available. This proves that the connection
|
||||
// did not simply take the first free port from the configured range.
|
||||
number, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("Reserve() after connection: %v", err)
|
||||
}
|
||||
|
||||
if number != 100 {
|
||||
t.Fatalf(
|
||||
"first free port after connection = %d, want 100",
|
||||
number,
|
||||
)
|
||||
}
|
||||
|
||||
manager.Release(number)
|
||||
|
||||
select {
|
||||
case conn := <-accepted:
|
||||
_ = conn.Close()
|
||||
|
||||
case <-t.Context().Done():
|
||||
t.Fatal("test context cancelled while waiting for data connection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectBridgeSuccess(t *testing.T) {
|
||||
dataAddress, accepted := startTestDataServer(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,19 @@
|
|||
* Beschreibung:
|
||||
* Verwaltung und Auswahl der für rs2322tcp reservierten virtuellen
|
||||
* seriellen Ports.
|
||||
*
|
||||
* Der VirtualPortManager verwaltet den für rs2322tcp vorgesehenen Bereich
|
||||
* von /dev/ttyUSBxxx-Schnittstellen.
|
||||
*
|
||||
* Ports können entweder automatisch über den nächsten freien Port oder
|
||||
* gezielt über eine vorgegebene Portnummer reserviert werden.
|
||||
*
|
||||
* Die gezielte Reservierung ist insbesondere für die aus client.json
|
||||
* geladene Konfiguration erforderlich. Dadurch bleibt eine Zuordnung wie
|
||||
*
|
||||
* /dev/ttyUSB100 -> radio
|
||||
*
|
||||
* auch technisch exakt erhalten.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
|
@ -64,6 +77,9 @@ func (m *VirtualPortManager) PortPath(number int) string {
|
|||
//
|
||||
// Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich
|
||||
// begonnen.
|
||||
//
|
||||
// Diese Methode bleibt für Fälle erhalten, in denen keine bestimmte
|
||||
// Portnummer vorgegeben ist.
|
||||
func (m *VirtualPortManager) Reserve() (int, error) {
|
||||
if m == nil {
|
||||
return 0, fmt.Errorf("virtual port manager is nil")
|
||||
|
|
@ -89,6 +105,42 @@ func (m *VirtualPortManager) Reserve() (int, error) {
|
|||
)
|
||||
}
|
||||
|
||||
// ReserveSpecific reserviert exakt den angegebenen virtuellen Port.
|
||||
//
|
||||
// Der Port muss innerhalb des konfigurierten Bereichs liegen und darf
|
||||
// momentan nicht bereits reserviert sein.
|
||||
//
|
||||
// Diese Methode wird für konfigurierte Zuordnungen verwendet, bei denen
|
||||
// die Portnummer aus client.json verbindlich ist.
|
||||
func (m *VirtualPortManager) ReserveSpecific(number int) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("virtual port manager is nil")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if number < m.first || number > m.last {
|
||||
return fmt.Errorf(
|
||||
"virtual port %d is outside configured range %d-%d",
|
||||
number,
|
||||
m.first,
|
||||
m.last,
|
||||
)
|
||||
}
|
||||
|
||||
if m.used[number] {
|
||||
return fmt.Errorf(
|
||||
"virtual port %d is already reserved",
|
||||
number,
|
||||
)
|
||||
}
|
||||
|
||||
m.used[number] = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release gibt einen zuvor reservierten Port wieder frei.
|
||||
func (m *VirtualPortManager) Release(number int) {
|
||||
if m == nil {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@
|
|||
* 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
|
||||
|
|
@ -140,6 +143,9 @@ func (p *ManagedVirtualPort) Close() error {
|
|||
//
|
||||
// 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")
|
||||
|
|
@ -150,6 +156,39 @@ func (m *VirtualPortManager) Open() (*ManagedVirtualPort, error) {
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -79,6 +79,59 @@ func TestVirtualPortManagerOpen(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerOpenSpecific(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
},
|
||||
)
|
||||
|
||||
port, err := manager.OpenSpecific(150)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenSpecific(150) failed: %v", err)
|
||||
}
|
||||
|
||||
if port == nil {
|
||||
t.Fatal("OpenSpecific(150) returned nil")
|
||||
}
|
||||
|
||||
if port.Path() != "/dev/ttyUSB150" {
|
||||
t.Errorf(
|
||||
"Path() = %q, want %q",
|
||||
port.Path(),
|
||||
"/dev/ttyUSB150",
|
||||
)
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(port.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
target, err := os.Readlink(linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Readlink() failed: %v", err)
|
||||
}
|
||||
|
||||
if target == "" {
|
||||
t.Fatal("internal virtual port link has empty target")
|
||||
}
|
||||
|
||||
if err := port.Close(); err != nil {
|
||||
t.Fatalf("Close() failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
||||
t.Fatalf(
|
||||
"internal link still exists after Close(): %q",
|
||||
linkPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@
|
|||
*
|
||||
* Beschreibung:
|
||||
* Tests für die Auswahl und Verwaltung virtueller serieller Ports.
|
||||
*
|
||||
* Die Tests decken sowohl die automatische Reservierung über Reserve()
|
||||
* als auch die gezielte Reservierung eines bestimmten Ports über
|
||||
* ReserveSpecific() ab.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
|
@ -201,6 +205,84 @@ func TestVirtualPortManagerPortPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerReserveSpecific(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
if err := manager.ReserveSpecific(150); err != nil {
|
||||
t.Fatalf("ReserveSpecific(150) failed: %v", err)
|
||||
}
|
||||
|
||||
if !manager.used[150] {
|
||||
t.Fatal("ReserveSpecific(150) did not reserve port 150")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerReserveSpecificOutsideRange(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
tests := []int{
|
||||
99,
|
||||
200,
|
||||
}
|
||||
|
||||
for _, number := range tests {
|
||||
if err := manager.ReserveSpecific(number); err == nil {
|
||||
t.Errorf(
|
||||
"ReserveSpecific(%d) succeeded, want range error",
|
||||
number,
|
||||
)
|
||||
}
|
||||
|
||||
if manager.used[number] {
|
||||
t.Errorf(
|
||||
"ReserveSpecific(%d) marked out-of-range port as used",
|
||||
number,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerReserveSpecificAlreadyReserved(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
if err := manager.ReserveSpecific(100); err != nil {
|
||||
t.Fatalf("first ReserveSpecific(100) failed: %v", err)
|
||||
}
|
||||
|
||||
if err := manager.ReserveSpecific(100); err == nil {
|
||||
t.Fatal("second ReserveSpecific(100) succeeded, want already-reserved error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerReserveSpecificAfterRelease(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
if err := manager.ReserveSpecific(150); err != nil {
|
||||
t.Fatalf("first ReserveSpecific(150) failed: %v", err)
|
||||
}
|
||||
|
||||
manager.Release(150)
|
||||
|
||||
if err := manager.ReserveSpecific(150); err != nil {
|
||||
t.Fatalf(
|
||||
"ReserveSpecific(150) after Release() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerNil(t *testing.T) {
|
||||
var manager *VirtualPortManager
|
||||
|
||||
|
|
@ -212,5 +294,9 @@ func TestVirtualPortManagerNil(t *testing.T) {
|
|||
t.Fatal("nil Reserve() succeeded, want error")
|
||||
}
|
||||
|
||||
if err := manager.ReserveSpecific(100); err == nil {
|
||||
t.Fatal("nil ReserveSpecific() succeeded, want error")
|
||||
}
|
||||
|
||||
manager.Release(100)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue