212 lines
5 KiB
Go
212 lines
5 KiB
Go
//go:build linux
|
|
|
|
/*
|
|
* ============================================================================
|
|
* Projekt.....: rs2322tcp
|
|
* Datei.......: internal/client/virtual_port_linux.go
|
|
* Copyright (C) 2026 Dieter Lang
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* Beschreibung:
|
|
* Linux-spezifische Verwaltung der internen Symlinks für virtuelle
|
|
* rs2322tcp-Seriellschnittstellen.
|
|
*
|
|
* Der öffentliche Gerätename /dev/ttyUSBxxx wird bei der Installation
|
|
* vorbereitet. Der Client verwaltet ausschließlich den benutzerbezogenen
|
|
* Link unter ~/.rs2322tcp/virtual/.
|
|
* ============================================================================
|
|
*/
|
|
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
|
)
|
|
|
|
const (
|
|
virtualDirectoryName = ".rs2322tcp/virtual"
|
|
)
|
|
|
|
// virtualPortDirectory returns the directory in the current user's home
|
|
// directory used for the internal virtual-port links.
|
|
func virtualPortDirectory() (string, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("get user home directory: %w", err)
|
|
}
|
|
|
|
return filepath.Join(home, virtualDirectoryName), nil
|
|
}
|
|
|
|
// virtualPortLinkPath returns the path of the internal link for a virtual
|
|
// serial port.
|
|
func virtualPortLinkPath(portPath string) (string, error) {
|
|
directory, err := virtualPortDirectory()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(directory, filepath.Base(portPath)), nil
|
|
}
|
|
|
|
// ensureVirtualPortDirectory creates the internal virtual-port directory
|
|
// if it does not already exist.
|
|
func ensureVirtualPortDirectory() error {
|
|
directory, err := virtualPortDirectory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(directory, 0755); err != nil {
|
|
return fmt.Errorf(
|
|
"create virtual port directory %q: %w",
|
|
directory,
|
|
err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setVirtualPortLink creates or replaces the internal link for a virtual
|
|
// port.
|
|
//
|
|
// The link itself is located below ~/.rs2322tcp/virtual/ and points to the
|
|
// current PTY, for example /dev/pts/2.
|
|
func setVirtualPortLink(portPath string, target string) error {
|
|
if portPath == "" {
|
|
return fmt.Errorf("virtual port path is empty")
|
|
}
|
|
|
|
if target == "" {
|
|
return fmt.Errorf("virtual port target is empty")
|
|
}
|
|
|
|
if err := ensureVirtualPortDirectory(); err != nil {
|
|
return err
|
|
}
|
|
|
|
linkPath, err := virtualPortLinkPath(portPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove an existing link. The installation-owned outer /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",
|
|
linkPath,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if err := os.Symlink(target, linkPath); err != nil {
|
|
return fmt.Errorf(
|
|
"create virtual port link %q -> %q: %w",
|
|
linkPath,
|
|
target,
|
|
err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeVirtualPortLink removes the internal link for a virtual port.
|
|
//
|
|
// The public /dev/ttyUSBxxx link is never modified.
|
|
func removeVirtualPortLink(portPath string) error {
|
|
if portPath == "" {
|
|
return fmt.Errorf("virtual port path is empty")
|
|
}
|
|
|
|
linkPath, err := virtualPortLinkPath(portPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf(
|
|
"remove existing virtual port link %q: %w",
|
|
linkPath,
|
|
err,
|
|
)
|
|
}
|
|
|
|
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
|
|
}
|