Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion serial_bsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ package serial
import "golang.org/x/sys/unix"

func (port *unixPort) Drain() error {
return unix.IoctlSetInt(port.handle, unix.TIOCDRAIN, 0)
return port.withGuard(func() error {
return unix.IoctlSetInt(port.handle, unix.TIOCDRAIN, 0)
})
}
8 changes: 6 additions & 2 deletions serial_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ func (port *unixPort) setSpecialBaudrate(speed uint32) error {
}

func (port *unixPort) ResetInputBuffer() error {
return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCIFLUSH)
return port.withGuard(func() error {
return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCIFLUSH)
})
}

func (port *unixPort) ResetOutputBuffer() error {
return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCOFLUSH)
return port.withGuard(func() error {
return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCOFLUSH)
})
}
10 changes: 6 additions & 4 deletions serial_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) {
}

func (port *unixPort) Drain() error {
// It's not super well documented, but this is the same as calling tcdrain:
// - https://git.musl-libc.org/cgit/musl/tree/src/termios/tcdrain.c
// - https://elixir.bootlin.com/linux/v6.2.8/source/drivers/tty/tty_io.c#L2673
return unix.IoctlSetInt(port.handle, unix.TCSBRK, 1)
return port.withGuard(func() error {
// It's not super well documented, but this is the same as calling tcdrain:
// - https://git.musl-libc.org/cgit/musl/tree/src/termios/tcdrain.c
// - https://elixir.bootlin.com/linux/v6.2.8/source/drivers/tty/tty_io.c#L2673
return unix.IoctlSetInt(port.handle, unix.TCSBRK, 1)
})
}
77 changes: 77 additions & 0 deletions serial_postclose_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// Copyright 2014-2026 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//

//go:build linux

package serial

import (
"errors"
"fmt"
"os"
"testing"
"time"

"golang.org/x/sys/unix"
)

// openPTYPort opens a Linux pseudo-terminal and returns a serial Port bound to
// its slave side. No real serial hardware is required.
func openPTYPort(t *testing.T) Port {
t.Helper()
master, err := os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0)
if err != nil {
t.Skipf("cannot open /dev/ptmx: %v", err)
}
t.Cleanup(func() { _ = master.Close() })
mfd := int(master.Fd())
if err := unix.IoctlSetPointerInt(mfd, unix.TIOCSPTLCK, 0); err != nil {
t.Skipf("TIOCSPTLCK: %v", err)
}
n, err := unix.IoctlGetInt(mfd, unix.TIOCGPTN)
if err != nil {
t.Skipf("TIOCGPTN: %v", err)
}
slave := fmt.Sprintf("/dev/pts/%d", n)
port, err := Open(slave, &Mode{BaudRate: 9600})
if err != nil {
t.Skipf("cannot open pty slave %s: %v", slave, err)
}
return port
}

// TestOperationsAfterCloseReturnPortClosed verifies that every method that
// touches the file descriptor returns PortClosed after the port is closed,
// instead of operating on a stale fd whose number the OS may have reassigned.
func TestOperationsAfterCloseReturnPortClosed(t *testing.T) {
port := openPTYPort(t)
if err := port.Close(); err != nil {
t.Fatalf("Close: %v", err)
}

assertClosed := func(name string, err error) {
t.Helper()
var pe *PortError
if !errors.As(err, &pe) || pe.Code() != PortClosed {
t.Errorf("%s after Close: got %v, want PortError{PortClosed}", name, err)
}
}

n, err := port.Write([]byte("data"))
if n != 0 {
t.Errorf("Write after Close: wrote %d bytes, want 0", n)
}
assertClosed("Write", err)
assertClosed("Drain", port.Drain())
assertClosed("ResetInputBuffer", port.ResetInputBuffer())
assertClosed("ResetOutputBuffer", port.ResetOutputBuffer())
assertClosed("SetMode", port.SetMode(&Mode{BaudRate: 9600}))
assertClosed("SetDTR", port.SetDTR(true))
assertClosed("SetRTS", port.SetRTS(false))
assertClosed("Break", port.Break(time.Millisecond))
_, err = port.GetModemStatusBits()
assertClosed("GetModemStatusBits", err)
}
8 changes: 6 additions & 2 deletions serial_resetbuf_linux_bsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ package serial
import "golang.org/x/sys/unix"

func (port *unixPort) ResetInputBuffer() error {
return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCIFLUSH)
return port.withGuard(func() error {
return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCIFLUSH)
})
}

func (port *unixPort) ResetOutputBuffer() error {
return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCOFLUSH)
return port.withGuard(func() error {
return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCOFLUSH)
})
}
164 changes: 98 additions & 66 deletions serial_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,26 @@ func (port *unixPort) Read(p []byte) (int, error) {
}
}

// withGuard runs op while the port is held open, mirroring the guard used by
// Read: it takes the close-lock read side and verifies the port is still open
// before op touches the file descriptor. Without it, a method called after (or
// concurrently with) Close would operate on a closed fd whose number the OS may
// already have reassigned to another file. Returns PortClosed when closed.
func (port *unixPort) withGuard(op func() error) error {
port.closeLock.RLock()
defer port.closeLock.RUnlock()
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
return op()
}

func (port *unixPort) Write(p []byte) (n int, err error) {
port.closeLock.RLock()
defer port.closeLock.RUnlock()
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
n, err = unix.Write(port.handle, p)
if n < 0 { // Do not return -1 unix errors
n = 0
Expand All @@ -118,76 +137,84 @@ func (port *unixPort) Write(p []byte) (n int, err error) {
}

func (port *unixPort) Break(t time.Duration) error {
if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil {
return err
}
return port.withGuard(func() error {
if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil {
return err
}

time.Sleep(t)
time.Sleep(t)

if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil {
return err
}
if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil {
return err
}

return nil
return nil
})
}

func (port *unixPort) SetMode(mode *Mode) error {
settings, err := port.getTermSettings()
if err != nil {
return err
}
if err := setTermSettingsParity(mode.Parity, settings); err != nil {
return err
}
if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil {
return err
}
if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil {
return err
}
requireSpecialBaudrate := false
if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil {
return err
} else if special {
requireSpecialBaudrate = true
}
if err := port.setTermSettings(settings); err != nil {
return err
}
if requireSpecialBaudrate {
// MacOSX require this one to be the last operation otherwise an
// 'Invalid serial port' error is produced.
if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil {
return port.withGuard(func() error {
settings, err := port.getTermSettings()
if err != nil {
return err
}
}
return nil
if err := setTermSettingsParity(mode.Parity, settings); err != nil {
return err
}
if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil {
return err
}
if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil {
return err
}
requireSpecialBaudrate := false
if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil {
return err
} else if special {
requireSpecialBaudrate = true
}
if err := port.setTermSettings(settings); err != nil {
return err
}
if requireSpecialBaudrate {
// MacOSX require this one to be the last operation otherwise an
// 'Invalid serial port' error is produced.
if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil {
return err
}
}
return nil
})
}

func (port *unixPort) SetDTR(dtr bool) error {
status, err := port.getModemBitsStatus()
if err != nil {
return err
}
if dtr {
status |= unix.TIOCM_DTR
} else {
status &^= unix.TIOCM_DTR
}
return port.setModemBitsStatus(status)
return port.withGuard(func() error {
status, err := port.getModemBitsStatus()
if err != nil {
return err
}
if dtr {
status |= unix.TIOCM_DTR
} else {
status &^= unix.TIOCM_DTR
}
return port.setModemBitsStatus(status)
})
}

func (port *unixPort) SetRTS(rts bool) error {
status, err := port.getModemBitsStatus()
if err != nil {
return err
}
if rts {
status |= unix.TIOCM_RTS
} else {
status &^= unix.TIOCM_RTS
}
return port.setModemBitsStatus(status)
return port.withGuard(func() error {
status, err := port.getModemBitsStatus()
if err != nil {
return err
}
if rts {
status |= unix.TIOCM_RTS
} else {
status &^= unix.TIOCM_RTS
}
return port.setModemBitsStatus(status)
})
}

func (port *unixPort) SetReadTimeout(timeout time.Duration) error {
Expand All @@ -199,16 +226,21 @@ func (port *unixPort) SetReadTimeout(timeout time.Duration) error {
}

func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) {
status, err := port.getModemBitsStatus()
if err != nil {
return nil, err
}
return &ModemStatusBits{
CTS: (status & unix.TIOCM_CTS) != 0,
DCD: (status & unix.TIOCM_CD) != 0,
DSR: (status & unix.TIOCM_DSR) != 0,
RI: (status & unix.TIOCM_RI) != 0,
}, nil
var bits *ModemStatusBits
err := port.withGuard(func() error {
status, err := port.getModemBitsStatus()
if err != nil {
return err
}
bits = &ModemStatusBits{
CTS: (status & unix.TIOCM_CTS) != 0,
DCD: (status & unix.TIOCM_CD) != 0,
DSR: (status & unix.TIOCM_DSR) != 0,
RI: (status & unix.TIOCM_RI) != 0,
}
return nil
})
return bits, err
}

func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
Expand Down
Loading