Support PinChangedEvent on Unix (dotnet/corefx#40383)
authorKrzysztof Wicher <mordotymoja@gmail.com>
Thu, 24 Oct 2019 22:06:34 +0000 (15:06 -0700)
committerGitHub <noreply@github.com>
Thu, 24 Oct 2019 22:06:34 +0000 (15:06 -0700)
* Support PinChangedEvent on Unix

* address PR feedback

* Fix typo in RaisePinChanged

Commit migrated from https://github.com/dotnet/corefx/commit/3dae29b60e6f0707b60f831ce77286de75ef8958

src/libraries/Native/Unix/System.IO.Ports.Native/pal_termios.c
src/libraries/Native/Unix/System.IO.Ports.Native/pal_termios.h
src/libraries/System.IO.Ports/src/Interop/Unix/Interop.Termios.cs
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Unix.cs
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.cs

index d9d4cb5..ece77cb 100644 (file)
@@ -32,11 +32,12 @@ enum
 /* Interop/Unix/Interop.Termios.cs */
 enum
 {
-    SignalDtr = 1,
-    SignalDsr = 2,
-    SignalRts = 3,
-    SignalCts = 4,
-    SignalDcd = 5,
+    SignalDtr = 1 << 0,
+    SignalDsr = 1 << 1,
+    SignalRts = 1 << 2,
+    SignalCts = 1 << 3,
+    SignalDcd = 1 << 4,
+    SignalRng = 1 << 5,
 };
 
 enum
@@ -83,6 +84,31 @@ int32_t SystemIoPortsNative_TermiosGetSignal(intptr_t handle, int32_t signal)
    }
 }
 
+int32_t SystemIoPortsNative_TermiosGetAllSignals(intptr_t handle)
+{
+    int32_t status = SystemIoPortsNative_TermiosGetStatus(handle);
+    if (status == -1)
+    {
+        return -1;
+    }
+
+    int32_t signals = 0;
+
+    if (status & TIOCM_CTS)
+        signals |= SignalCts;
+
+    if (status & TIOCM_DSR)
+        signals |= SignalDsr;
+
+    if (status & TIOCM_CAR)
+        signals |= SignalDcd;
+
+    if (status & TIOCM_RNG)
+        signals |= SignalRng;
+
+    return signals;
+}
+
 int32_t SystemIoPortsNative_TermiosSetSignal(intptr_t handle, int32_t signal, int32_t set)
 {
     int fd = ToFileDescriptor(handle);
index 4c4ab01..ae260a7 100644 (file)
@@ -7,6 +7,7 @@
 
 DLLEXPORT int32_t SystemIoPortsNative_TermiosGetSignal(intptr_t fd, int32_t signal);
 DLLEXPORT int32_t SystemIoPortsNative_TermiosSetSignal(intptr_t fd, int32_t signal, int32_t set);
+DLLEXPORT int32_t SystemIoPortsNative_TermiosGetAllSignals(intptr_t fd);
 
 DLLEXPORT int32_t SystemIoPortsNative_TermiosGetSpeed(intptr_t fd);
 DLLEXPORT int32_t SystemIoPortsNative_TermiosSetSpeed(intptr_t fd, int32_t speed);
index 1aa9d40..9a32096 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System;
 using System.IO.Ports;
 using System.Runtime.InteropServices;
 using Microsoft.Win32.SafeHandles;
@@ -10,13 +11,17 @@ internal static partial class Interop
 {
     internal static partial class Termios
     {
+        [Flags]
         internal enum Signals
         {
-            SignalDtr = 1,
-            SignalDsr = 2,
-            SignalRts = 3,
-            SignalCts = 4,
-            SignalDcd = 5,
+            None = 0,
+            SignalDtr = 1 << 0,
+            SignalDsr = 1 << 1,
+            SignalRts = 1 << 2,
+            SignalCts = 1 << 3,
+            SignalDcd = 1 << 4,
+            SignalRng = 1 << 5,
+            Error = -1,
         }
 
         internal enum Queue
@@ -35,6 +40,9 @@ internal static partial class Interop
         [DllImport(Libraries.IOPortsNative, EntryPoint = "SystemIoPortsNative_TermiosSetSignal", SetLastError = true)]
         internal static extern int TermiosGetSignal(SafeSerialDeviceHandle handle, Signals signal, int set);
 
+        [DllImport(Libraries.IOPortsNative, EntryPoint = "SystemIoPortsNative_TermiosGetAllSignals")]
+        internal static extern Signals TermiosGetAllSignals(SafeSerialDeviceHandle handle);
+
         [DllImport(Libraries.IOPortsNative, EntryPoint = "SystemIoPortsNative_TermiosSetSpeed", SetLastError = true)]
         internal static extern int TermiosSetSpeed(SafeSerialDeviceHandle handle, int speed);
 
index ab14ef8..785830d 100644 (file)
@@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
 using System.IO.Ports;
 using System.Threading;
 using System.Threading.Tasks;
+using Signals = Interop.Termios.Signals;
 
 namespace System.IO.Ports
 {
@@ -58,6 +59,26 @@ namespace System.IO.Ports
             }
         }
 
+        // called when any of the pin/ring-related triggers occurs
+        private SerialPinChangedEventHandler _pinChanged;
+        internal event SerialPinChangedEventHandler PinChanged
+        {
+            add
+            {
+                bool wasNull = _pinChanged == null;
+                _pinChanged += value;
+
+                if (wasNull)
+                {
+                    EnsureIOLoopRunning();
+                }
+            }
+            remove
+            {
+                _pinChanged -= value;
+            }
+        }
+
         // ----SECTION: inherited properties from Stream class ------------*
 
         // These six properties are required for SerialStream to inherit from the abstract Stream class.
@@ -611,6 +632,8 @@ namespace System.IO.Ports
 
             if (disposing)
             {
+                _dataReceived = null;
+                _pinChanged = null;
                 _ioLoop?.GetAwaiter().GetResult();
                 _ioLoop = null;
 
@@ -634,11 +657,18 @@ namespace System.IO.Ports
             {
                 ThreadPool.QueueUserWorkItem(s => {
                         var thisRef = (SerialStream)s;
-                        SerialDataReceivedEventHandler dataReceived = thisRef._dataReceived;
-                        if (dataReceived != null)
-                        {
-                            dataReceived(thisRef, new SerialDataReceivedEventArgs(SerialData.Chars));
-                        }
+                        thisRef._dataReceived?.Invoke(thisRef, new SerialDataReceivedEventArgs(SerialData.Chars));
+                    }, this);
+            }
+        }
+
+        private void RaisePinChanged(SerialPinChange pinChanged)
+        {
+            if (_pinChanged != null)
+            {
+                ThreadPool.QueueUserWorkItem(s => {
+                        var thisRef = (SerialStream)s;
+                        thisRef._pinChanged?.Invoke(thisRef, new SerialPinChangedEventArgs(pinChanged));
                     }, this);
             }
         }
@@ -765,13 +795,17 @@ namespace System.IO.Ports
             bool lastIsIdle = false;
             int ticksWhenIdleStarted = 0;
 
+            Signals lastSignals = _pinChanged != null ? Interop.Termios.TermiosGetAllSignals(_handle) : Signals.Error;
+
+            bool IsNoEventRegistered() => _dataReceived == null && _pinChanged == null;
+
             while (IsOpen && !eofReceived && !_ioLoopFinished)
             {
                 bool hasPendingReads = !_readQueue.IsEmpty;
                 bool hasPendingWrites = !_writeQueue.IsEmpty;
 
                 bool hasPendingIO = hasPendingReads || hasPendingWrites;
-                bool isIdle = _dataReceived == null && !hasPendingIO;
+                bool isIdle = IsNoEventRegistered() && !hasPendingIO;
 
                 if (!hasPendingIO)
                 {
@@ -790,7 +824,7 @@ namespace System.IO.Ports
                             lock (_ioLoopLock)
                             {
                                 // double check we are done under lock
-                                if (_dataReceived == null && _readQueue.IsEmpty && _writeQueue.IsEmpty)
+                                if (IsNoEventRegistered() && _readQueue.IsEmpty && _writeQueue.IsEmpty)
                                 {
                                     _ioLoop = null;
                                     break;
@@ -850,10 +884,52 @@ namespace System.IO.Ports
                     RaiseDataReceivedChars();
                 }
 
+                if (_pinChanged != null)
+                {
+                    // Checking for changes could technically speaking be done by waiting with ioctl+TIOCMIWAIT
+                    // This would require spinning new thread and also would potentially trigger events when
+                    // user didn't have time to respond.
+                    // Diffing seems like a better solution.
+                    Signals current = Interop.Termios.TermiosGetAllSignals(_handle);
+
+                    // There is no really good action we can take when this errors so just ignore
+                    // a sinle event.
+                    if (current != Signals.Error && lastSignals != Signals.Error)
+                    {
+                        Signals changed = current ^ lastSignals;
+                        if (changed != Signals.None)
+                        {
+                            SerialPinChange pinChanged = SignalsToPinChanges(changed);
+                            RaisePinChanged(pinChanged);
+                        }
+                    }
+
+                    lastSignals = current;
+                }
+
                 lastIsIdle = isIdle;
             }
         }
 
+        private static SerialPinChange SignalsToPinChanges(Signals signals)
+        {
+            SerialPinChange pinChanges = default;
+
+            if (signals.HasFlag(Signals.SignalCts))
+                pinChanges |= SerialPinChange.CtsChanged;
+
+            if (signals.HasFlag(Signals.SignalDsr))
+                pinChanges |= SerialPinChange.DsrChanged;
+
+            if (signals.HasFlag(Signals.SignalDcd))
+                pinChanges |= SerialPinChange.CDChanged;
+
+            if (signals.HasFlag(Signals.SignalRng))
+                pinChanges |= SerialPinChange.Ring;
+
+            return pinChanges;
+        }
+
         private static CancellationTokenSource GetCancellationTokenSourceFromTimeout(int timeoutMs)
         {
             return timeoutMs == SerialPort.InfiniteTimeout ?
index 2affb90..d634115 100644 (file)
@@ -36,6 +36,9 @@ namespace System.IO.Ports
         // called when one character is received.
         internal event SerialDataReceivedEventHandler DataReceived;
 
+        // called when any of the pin/ring-related triggers occurs
+        internal event SerialPinChangedEventHandler PinChanged;
+
         private SafeFileHandle _handle = null;
 
         // members supporting properties exposed to SerialPort
index f8f3fe4..e0a2cd8 100644 (file)
@@ -22,8 +22,6 @@ namespace System.IO.Ports
         private Handshake _handshake;
 
 #pragma warning disable CS0067 // Events shared by Windows and Linux, on Linux we currently never call them
-        // called when any of the pin/ring-related triggers occurs
-        internal event SerialPinChangedEventHandler PinChanged;
         // called when any runtime error occurs on the port (frame, overrun, parity, etc.)
         internal event SerialErrorReceivedEventHandler ErrorReceived;
 #pragma warning restore CS0067