234 lines
5.1 KiB
Go
234 lines
5.1 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")
|
|
}
|
|
|
|
return unix.Write(fd, b)
|
|
}
|
|
|
|
// 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
|
|
}
|