From f355dbf6081e7f1e8b12d832608eea18a91ef302 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Sun, 12 Mar 2017 22:44:28 +0200 Subject: [PATCH 1/4] windows: correcly signal PortClosed error on blocked Read() This commit fix regression test TestConcurrentReadAndWrite. Even if the test checks purpose is another it has detected anyway that the returned error is incorrect. Another specific test for closing port detection will be added in the future. --- serial_windows.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/serial_windows.go b/serial_windows.go index ec328f9..46803c6 100644 --- a/serial_windows.go +++ b/serial_windows.go @@ -82,14 +82,15 @@ func (port *windowsPort) Read(p []byte) (int, error) { defer syscall.CloseHandle(ev.HEvent) for { err := syscall.ReadFile(port.handle, p, &readed, ev) + if err == syscall.ERROR_IO_PENDING { + err = getOverlappedResult(port.handle, ev, &readed, true) + } switch err { case nil: // operation completed successfully - case syscall.ERROR_IO_PENDING: - // wait for overlapped I/O to complete - if err := getOverlappedResult(port.handle, ev, &readed, true); err != nil { - return int(readed), err - } + case syscall.ERROR_OPERATION_ABORTED: + // port may have been closed + return int(readed), &PortError{code: PortClosed, causedBy: err} default: // error happened return int(readed), err From a63b28875ff5b7f52b2c41992656afac36c66a34 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Thu, 27 Jul 2017 13:53:59 +0200 Subject: [PATCH 2/4] linux: detect port disconnection during Read See https://stackoverflow.com/a/34945814/1655275 > pselect signals that file descriptor is ready and > ioctl(fd, FIONREAD, &len) returns zero len. Failure to detect this condition has been revealed by the testsuite: === RUN TestDisconnectingPortDetection 2017/07/27 13:56:37 PR - Connecting to Probe 2017/07/27 13:56:37 > Searching for port 2341:8037 2017/07/27 13:56:37 Detected port '/dev/ttyACM0' 2341:8037 2017/07/27 13:56:37 Using '/dev/ttyACM0' 2017/07/27 13:56:37 Starting test (timeout 20s) 2017/07/27 13:56:37 PR - Turn ON target 2017/07/27 13:56:37 TR - Connecting to Target 2017/07/27 13:56:37 > Searching for port 2341:8036 2017/07/27 13:56:37 Detected port '/dev/ttyACM0' 2341:8037 [...cut...] 2017/07/27 13:56:46 > Searching for port 2341:8036 2017/07/27 13:56:46 Detected port '/dev/ttyACM0' 2341:8037 2017/07/27 13:56:46 Detected port '/dev/ttyACM1' 2341:8036 2017/07/27 13:56:46 Using '/dev/ttyACM1' 2017/07/27 13:56:46 T2 - Make a Read call 2017/07/27 13:56:46 T1 - Delay 200ms before disconnecting target 2017/07/27 13:56:46 T1 - Disconnect target 2017/07/27 13:56:46 PR - Turn OFF target 2017/07/27 13:56:46 T2 - Read returned: n=0 err=nil --- FAIL: TestDisconnectingPortDetection (9.18s) Error Trace: serial_test.go:100 Error: An error is expected but got nil. %s Messages: Read returned no errors this commit fix the problem above. --- serial_unix.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/serial_unix.go b/serial_unix.go index f9e887c..c921389 100644 --- a/serial_unix.go +++ b/serial_unix.go @@ -77,6 +77,12 @@ func (port *unixPort) Read(p []byte) (int, error) { if err == unix.EINTR { continue } + // Linux: when the port is disconnected during a read operation + // the port is left in a "readable with zero-length-data" state. + // https://stackoverflow.com/a/34945814/1655275 + if n == 0 && err == nil { + return 0, &PortError{code: PortClosed} + } if n < 0 { // Do not return -1 unix errors n = 0 } From 2cb14f049cbdf8b8852253a1a8faf2c24699636d Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Sun, 10 May 2020 23:42:16 +0200 Subject: [PATCH 3/4] SetReadTimeout: unix implementation --- serial.go | 13 +++++++++++++ serial_unix.go | 30 +++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/serial.go b/serial.go index d50e1f7..08f227a 100644 --- a/serial.go +++ b/serial.go @@ -6,6 +6,8 @@ package serial +import "time" + //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go // Port is the interface for a serial Port @@ -40,10 +42,17 @@ type Port interface { // modem status bits for the serial port (CTS, DSR, etc...) GetModemStatusBits() (*ModemStatusBits, error) + // SetReadTimeout sets the timeout for the Read operation or use serial.NoTimeout + // to disable read timeout. + SetReadTimeout(t time.Duration) error + // Close the serial port Close() error } +// NoTimeout should be used as a parameter to SetReadTimeout to disable timeout. +var NoTimeout time.Duration = -1 + // ModemStatusBits contains all the modem status bits for a serial port (CTS, DSR, etc...). // It can be retrieved with the Port.GetModemStatusBits() method. type ModemStatusBits struct { @@ -125,6 +134,8 @@ const ( InvalidParity // InvalidStopBits the selected number of stop bits is not valid or not supported InvalidStopBits + // InvalidTimeoutValue the timeout value is not valid or not supported + InvalidTimeoutValue // ErrorEnumeratingPorts an error occurred while listing serial port ErrorEnumeratingPorts // PortClosed the port has been closed while the operation is in progress @@ -152,6 +163,8 @@ func (e PortError) EncodedErrorString() string { return "Port parity invalid or not supported" case InvalidStopBits: return "Port stop bits invalid or not supported" + case InvalidTimeoutValue: + return "Timeout value invalid or not supported" case ErrorEnumeratingPorts: return "Could not enumerate serial ports" case PortClosed: diff --git a/serial_unix.go b/serial_unix.go index c921389..0143aa2 100644 --- a/serial_unix.go +++ b/serial_unix.go @@ -14,6 +14,7 @@ import ( "strings" "sync" "sync/atomic" + "time" "go.bug.st/serial/unixutils" "golang.org/x/sys/unix" @@ -22,6 +23,7 @@ import ( type unixPort struct { handle int + readTimeout time.Duration closeLock sync.RWMutex closeSignal *unixutils.Pipe opened uint32 @@ -61,9 +63,18 @@ func (port *unixPort) Read(p []byte) (int, error) { return 0, &PortError{code: PortClosed} } + var deadline time.Time + if port.readTimeout != NoTimeout { + deadline = time.Now().Add(port.readTimeout) + } + fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD()) for { - res, err := unixutils.Select(fds, nil, fds, -1) + timeout := time.Duration(-1) + if port.readTimeout != NoTimeout { + timeout = deadline.Sub(time.Now()) + } + res, err := unixutils.Select(fds, nil, fds, timeout) if err == unix.EINTR { continue } @@ -73,6 +84,10 @@ func (port *unixPort) Read(p []byte) (int, error) { if res.IsReadable(port.closeSignal.ReadFD()) { return 0, &PortError{code: PortClosed} } + if !res.IsReadable(port.handle) { + // Timeout happened + return 0, nil + } n, err := unix.Read(port.handle, p) if err == unix.EINTR { continue @@ -152,6 +167,14 @@ func (port *unixPort) SetRTS(rts bool) error { return port.setModemBitsStatus(status) } +func (port *unixPort) SetReadTimeout(timeout time.Duration) error { + if timeout < 0 && timeout != NoTimeout { + return &PortError{code: InvalidTimeoutValue} + } + port.readTimeout = timeout + return nil +} + func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) { status, err := port.getModemBitsStatus() if err != nil { @@ -177,8 +200,9 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) { return nil, err } port := &unixPort{ - handle: h, - opened: 1, + handle: h, + opened: 1, + readTimeout: NoTimeout, } // Setup serial port From 23dbc8b9ba7d733ce0f3902715bdf61891790de6 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 11 May 2020 00:01:36 +0100 Subject: [PATCH 4/4] SetReadTimeout: windows implementation --- serial_windows.go | 54 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/serial_windows.go b/serial_windows.go index 46803c6..6b6d0ee 100644 --- a/serial_windows.go +++ b/serial_windows.go @@ -18,13 +18,15 @@ package serial */ import ( - "syscall" "sync" + "syscall" + "time" ) type windowsPort struct { - mu sync.Mutex - handle syscall.Handle + mu sync.Mutex + handle syscall.Handle + readTimeoutCycles int64 } func nativeGetPortsList() ([]string, error) { @@ -80,6 +82,8 @@ func (port *windowsPort) Read(p []byte) (int, error) { return 0, err } defer syscall.CloseHandle(ev.HEvent) + + cycles := int64(0) for { err := syscall.ReadFile(port.handle, p, &readed, ev) if err == syscall.ERROR_IO_PENDING { @@ -103,6 +107,14 @@ func (port *windowsPort) Read(p []byte) (int, error) { return 0, err } + if port.readTimeoutCycles != -1 { + cycles++ + if cycles == port.readTimeoutCycles { + // Timeout + return 0, nil + } + } + // At the moment it seems that the only reliable way to check if // a serial port is alive in Windows is to check if the SetCommState // function fails. @@ -370,6 +382,31 @@ func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) { }, nil } +func (port *windowsPort) SetReadTimeout(timeout time.Duration) error { + var cycles, cycleDuration int64 + if timeout == NoTimeout { + cycles = -1 + cycleDuration = 1000 + } else { + cycles = timeout.Milliseconds()/1000 + 1 + cycleDuration = timeout.Milliseconds() / cycles + } + + err := setCommTimeouts(port.handle, &commTimeouts{ + ReadIntervalTimeout: 0xFFFFFFFF, + ReadTotalTimeoutMultiplier: 0xFFFFFFFF, + ReadTotalTimeoutConstant: uint32(cycleDuration), + WriteTotalTimeoutConstant: 0, + WriteTotalTimeoutMultiplier: 0, + }) + if err != nil { + return &PortError{code: InvalidTimeoutValue, causedBy: err} + } + port.readTimeoutCycles = cycles + + return nil +} + func createOverlappedEvent() (*syscall.Overlapped, error) { h, err := createEvent(nil, true, false, nil) return &syscall.Overlapped{HEvent: h}, err @@ -435,18 +472,9 @@ func nativeOpen(portName string, mode *Mode) (*windowsPort, error) { return nil, &PortError{code: InvalidSerialPort} } - // Set timeouts to 1 second - timeouts := &commTimeouts{ - ReadIntervalTimeout: 0xFFFFFFFF, - ReadTotalTimeoutMultiplier: 0xFFFFFFFF, - ReadTotalTimeoutConstant: 1000, // 1 sec - WriteTotalTimeoutConstant: 0, - WriteTotalTimeoutMultiplier: 0, - } - if setCommTimeouts(port.handle, timeouts) != nil { + if port.SetReadTimeout(NoTimeout) != nil { port.Close() return nil, &PortError{code: InvalidSerialPort} } - return port, nil }