public interface ISerialPortIo : IDisposable { string PortName { get; } string ReadLine(); void WriteLine(string text); } public class SerialPortConfig { public string Name { get; private set; } public int BaudRate { get; private set; } public int DataBits { get; private set; } public StopBits StopBits { get; private set; } public Parity Parity { get; private set; } public bool DtrEnable { get; private set; } public bool RtsEnable { get; private set; } public SerialPortConfig( string name, int baudRate, int dataBits, StopBits stopBits, Parity parity, bool dtrEnable, bool rtsEnable) { if (String.IsNullOrWhiteSpace(name)) throw new ArgumentNullException("name"); this.RtsEnable = rtsEnable; this.BaudRate = baudRate; this.DataBits = dataBits; this.StopBits = stopBits; this.Parity = parity; this.DtrEnable = dtrEnable; this.Name = name; } public override string ToString() { return String.Format( "{0} (Baud: {1}/DataBits: {2}/Parity: {3}/StopBits: {4}/{5})", this.Name, this.BaudRate, this.DataBits, this.Parity, this.StopBits, this.RtsEnable ? "RTS" : "No RTS"); } } // Wrapper around SerialPort public class SerialPortIo : ISerialPortIo { protected ILog Log { get; private set; } static readonly ILog s_Log = LogManager.GetLogger(typeof(SerialPortIo)); readonly SerialPort _port; readonly Stream _internalSerialStream; public SerialPortIo(SerialPortConfig portConfig) { if (portConfig == null) throw new ArgumentNullException("portConfig"); this.Log = LogManager.GetLogger(this.GetType()); // http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html SerialPortFixer.Execute(portConfig.Name); var port = new SerialPort( portConfig.Name, portConfig.BaudRate, portConfig.Parity, portConfig.DataBits, portConfig.StopBits) { RtsEnable = portConfig.RtsEnable, DtrEnable = portConfig.DtrEnable, ReadTimeout = 5000, WriteTimeout = 5000 }; port.Open(); try { this._internalSerialStream = port.BaseStream; this._port = port; this._port.DiscardInBuffer(); this._port.DiscardOutBuffer(); } catch (Exception ex) { Stream internalStream = this._internalSerialStream; if (internalStream == null) { FieldInfo field = typeof(SerialPort).GetField( "internalSerialStream", BindingFlags.Instance | BindingFlags.NonPublic); // This will happen if the SerialPort class is changed // in future versions of the .NET Framework if (field == null) { this.Log.WarnFormat( "An exception occured while creating the serial port adaptor, " + "the internal stream reference was not acquired and we were unable " + "to get it using reflection. The serial port may not be accessible " + "any further until the serial port object finalizer has been run: {0}", ex); throw; } internalStream = (Stream)field.GetValue(port); } this.Log.DebugFormat( "An error occurred while constructing the serial port adaptor: {0}", ex); SafeDisconnect(port, internalStream); throw; } } public string PortName { get { return this._port.PortName; } } public string ReadLine() { return this._port.ReadTo(Environment.NewLine); } public void WriteLine(string text) { this._port.Write(text); this._port.Write("\r"); } public void Dispose() { this.Dispose(true); } protected void Dispose(bool disposing) { SafeDisconnect(this._port, this._internalSerialStream); if (disposing) { GC.SuppressFinalize(this); } } /// /// Safely closes a serial port and its internal stream even if /// a USB serial interface was physically removed from the system /// in a reliable manner. /// /// /// /// /// The class has 3 different problems in disposal /// in case of a USB serial device that is physically removed: /// /// 1. The eventLoopRunner is asked to stop and /// returns false. Upon disposal this property is checked and closing /// the internal serial stream is skipped, thus keeping the original /// handle open indefinitely (until the finalizer runs which leads to the next problem) /// /// The solution for this one is to manually close the internal serial stream. /// We can get its reference by /// before the exception has happened or by reflection and getting the /// "internalSerialStream" field. /// /// 2. Closing the internal serial stream throws an exception and closes /// the internal handle without waiting for its eventLoopRunner thread to finish, /// causing an uncatchable ObjectDisposedException from it later on when the finalizer /// runs (which oddly avoids throwing the exception but still fails to wait for /// the eventLoopRunner). /// /// The solution is to manually ask the event loop runner thread to shutdown /// (via reflection) and waiting for it before closing the internal serial stream. /// /// 3. Since Dispose throws exceptions, the finalizer is not suppressed. /// /// The solution is to suppress their finalizers at the beginning. /// static void SafeDisconnect(SerialPort port, Stream internalSerialStream) { GC.SuppressFinalize(port); GC.SuppressFinalize(internalSerialStream); ShutdownEventLoopHandler(internalSerialStream); try { s_Log.DebugFormat("Disposing internal serial stream"); internalSerialStream.Close(); } catch (Exception ex) { s_Log.DebugFormat( "Exception in serial stream shutdown of port {0}: {1}", port.PortName, ex); } try { s_Log.DebugFormat("Disposing serial port"); port.Close(); } catch (Exception ex) { s_Log.DebugFormat("Exception in port {0} shutdown: {1}", port.PortName, ex); } } static void ShutdownEventLoopHandler(Stream internalSerialStream) { try { s_Log.DebugFormat("Working around .NET SerialPort class Dispose bug"); FieldInfo eventRunnerField = internalSerialStream.GetType() .GetField("eventRunner", BindingFlags.NonPublic | BindingFlags.Instance); if (eventRunnerField == null) { s_Log.WarnFormat( "Unable to find EventLoopRunner field. " + "SerialPort workaround failure. Application may crash after " + "disposing SerialPort unless .NET 1.1 unhandled exception " + "policy is enabled from the application's config file."); } else { object eventRunner = eventRunnerField.GetValue(internalSerialStream); Type eventRunnerType = eventRunner.GetType(); FieldInfo endEventLoopFieldInfo = eventRunnerType.GetField( "endEventLoop", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo eventLoopEndedSignalFieldInfo = eventRunnerType.GetField( "eventLoopEndedSignal", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo waitCommEventWaitHandleFieldInfo = eventRunnerType.GetField( "waitCommEventWaitHandle", BindingFlags.Instance | BindingFlags.NonPublic); if (endEventLoopFieldInfo == null || eventLoopEndedSignalFieldInfo == null || waitCommEventWaitHandleFieldInfo == null) { s_Log.WarnFormat( "Unable to find the EventLoopRunner internal wait handle or loop signal fields. " + "SerialPort workaround failure. Application may crash after " + "disposing SerialPort unless .NET 1.1 unhandled exception " + "policy is enabled from the application's config file."); } else { s_Log.DebugFormat( "Waiting for the SerialPort internal EventLoopRunner thread to finish..."); var eventLoopEndedWaitHandle = (WaitHandle)eventLoopEndedSignalFieldInfo.GetValue(eventRunner); var waitCommEventWaitHandle = (ManualResetEvent)waitCommEventWaitHandleFieldInfo.GetValue(eventRunner); endEventLoopFieldInfo.SetValue(eventRunner, true); // Sometimes the event loop handler resets the wait handle // before exiting the loop and hangs (in case of USB disconnect) // In case it takes too long, brute-force it out of its wait by // setting the handle again. do { waitCommEventWaitHandle.Set(); } while (!eventLoopEndedWaitHandle.WaitOne(2000)); s_Log.DebugFormat("Wait completed. Now it is safe to continue disposal."); } } } catch (Exception ex) { s_Log.ErrorFormat( "SerialPort workaround failure. Application may crash after " + "disposing SerialPort unless .NET 1.1 unhandled exception " + "policy is enabled from the application's config file: {0}", ex); } } }