rs2322tcp/internal/client/pty_linux.go

257 lines
5.3 KiB
Go

//go:build linux
/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/client/pty_linux.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Linux-spezifische Implementierung der virtuellen seriellen Schnittstelle
* über ein PTY-Paar.
*
* Der PTY-Master wird intern vom Client verwendet. Der zugehörige PTY-Slave
* wird über den virtuellen Port als serielle Schnittstelle bereitgestellt.
* ============================================================================
*/
package client
import (
"fmt"
"sync/atomic"
"golang.org/x/sys/unix"
)
///////////////////////////////////////////////////////////////////////////////
// ptySerial
///////////////////////////////////////////////////////////////////////////////
// ptySerial implements VirtualSerial using a Linux PTY pair.
//
// The file descriptors are stored atomically because Read/Write may execute
// concurrently with Close. Close must be able to invalidate the descriptors
// without racing with a concurrent Read or Write.
type ptySerial struct {
masterFD atomic.Int64
slaveFD atomic.Int64
path string
}
///////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////
// newVirtualSerial creates a new Linux PTY pair.
func newVirtualSerial() (VirtualSerial, error) {
masterFD, err := unix.Open(
"/dev/ptmx",
unix.O_RDWR|unix.O_NOCTTY,
0,
)
if err != nil {
return nil, fmt.Errorf("open PTY master: %w", err)
}
// Wenn die weitere Initialisierung fehlschlägt,
// darf der Master nicht offen bleiben.
closeMaster := true
defer func() {
if closeMaster {
_ = unix.Close(masterFD)
}
}()
// PTY-Slave entsperren.
if err := unix.IoctlSetPointerInt(
masterFD,
unix.TIOCSPTLCK,
0,
); err != nil {
return nil, fmt.Errorf("unlock PTY slave: %w", err)
}
// PTY-Nummer ermitteln.
ptsNumber, err := unix.IoctlGetInt(
masterFD,
unix.TIOCGPTN,
)
if err != nil {
return nil, fmt.Errorf("get PTY number: %w", err)
}
path := fmt.Sprintf("/dev/pts/%d", ptsNumber)
// Slave öffnen, um die Terminalparameter konfigurieren zu können.
slaveFD, err := unix.Open(
path,
unix.O_RDWR|unix.O_NOCTTY,
0,
)
if err != nil {
return nil, fmt.Errorf(
"open PTY slave %s: %w",
path,
err,
)
}
closeSlave := true
defer func() {
if closeSlave {
_ = unix.Close(slaveFD)
}
}()
if err := configurePTY(slaveFD); err != nil {
return nil, err
}
serial := &ptySerial{
path: path,
}
serial.masterFD.Store(int64(masterFD))
serial.slaveFD.Store(int64(slaveFD))
closeMaster = false
closeSlave = false
return serial, nil
}
///////////////////////////////////////////////////////////////////////////////
// PTY configuration
///////////////////////////////////////////////////////////////////////////////
func configurePTY(fd int) error {
termios, err := unix.IoctlGetTermios(
fd,
unix.TCGETS,
)
if err != nil {
return fmt.Errorf("get PTY termios: %w", err)
}
// Raw Mode:
// keine Canonical-Verarbeitung,
// kein Echo,
// keine Signalinterpretation,
// keine CR/LF-Konvertierung,
// keine XON/XOFF-Verarbeitung.
termios.Iflag &^= unix.IGNBRK |
unix.BRKINT |
unix.PARMRK |
unix.ISTRIP |
unix.INLCR |
unix.IGNCR |
unix.ICRNL |
unix.IXON
termios.Oflag &^= unix.OPOST
termios.Lflag &^= unix.ECHO |
unix.ECHONL |
unix.ICANON |
unix.ISIG |
unix.IEXTEN
termios.Cflag &^= unix.CSIZE | unix.PARENB
termios.Cflag |= unix.CS8
// Byte-orientiertes, blockierendes Lesen.
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(
fd,
unix.TCSETS,
termios,
); err != nil {
return fmt.Errorf("set PTY raw mode: %w", err)
}
return nil
}
///////////////////////////////////////////////////////////////////////////////
// VirtualSerial implementation
///////////////////////////////////////////////////////////////////////////////
// Path returns the PTY slave path.
func (p *ptySerial) Path() string {
return p.path
}
// Read reads raw bytes from the PTY master.
func (p *ptySerial) Read(b []byte) (int, error) {
fd := int(p.masterFD.Load())
if fd < 0 {
return 0, fmt.Errorf("PTY master is closed")
}
return unix.Read(fd, b)
}
// Write writes raw bytes to the PTY master.
func (p *ptySerial) Write(b []byte) (int, error) {
fd := int(p.masterFD.Load())
if fd < 0 {
return 0, fmt.Errorf("PTY master is closed")
}
fmt.Printf(
"PTY TX [%s]: % X\n",
p.path,
b,
)
n, err := unix.Write(fd, b)
if err != nil {
fmt.Printf(
"PTY TX Fehler [%s]: %v\n",
p.path,
err,
)
return n, err
}
fmt.Printf(
"PTY TX erfolgreich [%s]: %d Bytes\n",
p.path,
n,
)
return n, nil
}
// Close closes the PTY master and slave.
//
// The descriptors are invalidated atomically before closing them so that
// concurrent Read or Write calls do not access the descriptor fields
// concurrently with Close.
func (p *ptySerial) Close() error {
var firstErr error
masterFD := int(p.masterFD.Swap(-1))
if masterFD >= 0 {
if err := unix.Close(masterFD); err != nil {
firstErr = err
}
}
slaveFD := int(p.slaveFD.Swap(-1))
if slaveFD >= 0 {
if err := unix.Close(slaveFD); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}