//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 }