2

I'm trying really hard to use a SerialPort in C# and found out the API for serial ports in .NET has bugs in it since 2010. I have combined the patches made in this blog post and that answer.

However, I'm still getting an unhandled exception in the GC thread that crashes my program when I try closing my Serial port. Amusingly, the top of the Stack trace is exactly in SerialStream's Finalize() method even if the SerialStream object has already been GC.SuppressFinalize() by the code of the answer I linked.

Here's the exception:

System.ObjectDisposedException: Safe handle has been closed
  at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success)
  at System.StubHelpers.StubHelpers.SafeHandleAddRef(SafeHandle pHandle, Boolean& success)
  at Microsoft.Win32.UnsafeNativeMethods.SetCommMask(SafeFileHandle hFile, Int32 dwEvtMask)
  at System.IO.Ports.SerialStream.Dispose(Boolean disposing)
  at System.IO.Ports.SerialStream.Finalize()

Here's the logs for the serial port debugging:

2017-20-07 14:29:22, Debug, Working around .NET SerialPort class Dispose bug 
2017-20-07 14:29:22, Debug, Waiting for the SerialPort internal EventLoopRunner thread to finish...
2017-20-07 14:29:22, Debug, Wait completed. Now it is safe to continue disposal. 
2017-20-07 14:29:22, Debug, Disposing internal serial stream 
2017-20-07 14:29:22, Debug, Disposing serial port

Here's my code, a slightly modified version of the 2 links I posted:

public static class SerialPortFixerExt
{
    public static void SafeOpen(this SerialPort port)
    {
        using(new SerialPortFixer(port.PortName))
        {
        }

        port.Open();
    }

    public static void SafeClose(this SerialPort port, Logger logger)
    {
        try
        {
            Stream internalSerialStream = (Stream)port.GetType()
                .GetField("internalSerialStream", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(port);

            GC.SuppressFinalize(port);
            GC.SuppressFinalize(internalSerialStream);

            ShutdownEventLoopHandler(internalSerialStream, logger);

            try
            {
                logger.Debug("serial-port-debug", "Disposing internal serial stream");
                internalSerialStream.Close();
            }
            catch(Exception ex)
            {
                logger.Debug("serial-port-debug", String.Format("Exception in serial stream shutdown of port {0}: ", port.PortName), ex);
            }

            try
            {
                logger.Debug("serial-port-debug", "Disposing serial port");
                port.Close();
            }
            catch(Exception ex)
            {
                logger.Debug("serial-port-debug", String.Format("Exception in port {0} shutdown: ", port.PortName), ex);
            }

        }
        catch(Exception ex)
        {
            logger.Debug("serial-port-debug", String.Format("Exception in port {0} shutdown: ", port.PortName), ex);
        }
    }

    static void ShutdownEventLoopHandler(Stream internalSerialStream, Logger logger)
    {
        try
        {
            logger.Debug("serial-port-debug", "Working around .NET SerialPort class Dispose bug");

            FieldInfo eventRunnerField = internalSerialStream.GetType()
                .GetField("eventRunner", BindingFlags.NonPublic | BindingFlags.Instance);

            if(eventRunnerField == null)
            {
                logger.Warning("serial-port-debug",
                    "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)
                {
                    logger.Warning("serial-port-debug",
                        "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
                {
                    logger.Debug("serial-port-debug",
                        "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));

                    logger.Debug("serial-port-debug", "Wait completed. Now it is safe to continue disposal.");
                }
            }
        }
        catch(Exception ex)
        {
            logger.Warning("serial-port-debug",
                "SerialPort workaround failure. Application may crash after "
                + "disposing SerialPort unless .NET 1.1 unhandled exception "
                + "policy is enabled from the application's config file: " +
                ex);
        }
    }
}

public class SerialPortFixer : IDisposable
{
    #region IDisposable Members

    public void Dispose()
    {
        if(m_Handle != null)
        {
            m_Handle.Close();
            m_Handle = null;
        }
    }

    #endregion

    #region Implementation

    private const int DcbFlagAbortOnError = 14;
    private const int CommStateRetries = 10;
    private SafeFileHandle m_Handle;

    internal SerialPortFixer(string portName)
    {
        const int dwFlagsAndAttributes = 0x40000000;
        const int dwAccess = unchecked((int)0xC0000000);

        if((portName == null) || !portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase))
        {
            throw new ArgumentException("Invalid Serial Port", "portName");
        }
        SafeFileHandle hFile = CreateFile(@"\\.\" + portName, dwAccess, 0, IntPtr.Zero, 3, dwFlagsAndAttributes,
            IntPtr.Zero);
        if(hFile.IsInvalid)
        {
            WinIoError();
        }
        try
        {
            int fileType = GetFileType(hFile);
            if((fileType != 2) && (fileType != 0))
            {
                throw new ArgumentException("Invalid Serial Port", "portName");
            }
            m_Handle = hFile;
            InitializeDcb();
        }
        catch
        {
            hFile.Close();
            m_Handle = null;
            throw;
        }
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int FormatMessage(int dwFlags, HandleRef lpSource, int dwMessageId, int dwLanguageId,
        StringBuilder lpBuffer, int nSize, IntPtr arguments);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool GetCommState(SafeFileHandle hFile, ref Dcb lpDcb);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool SetCommState(SafeFileHandle hFile, ref Dcb lpDcb);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern bool ClearCommError(SafeFileHandle hFile, ref int lpErrors, ref Comstat lpStat);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
        IntPtr securityAttrs, int dwCreationDisposition,
        int dwFlagsAndAttributes, IntPtr hTemplateFile);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern int GetFileType(SafeFileHandle hFile);

    private void InitializeDcb()
    {
        Dcb dcb = new Dcb();
        GetCommStateNative(ref dcb);
        dcb.Flags &= ~(1u << DcbFlagAbortOnError);
        SetCommStateNative(ref dcb);
    }

    private static string GetMessage(int errorCode)
    {
        StringBuilder lpBuffer = new StringBuilder(0x200);
        if(
            FormatMessage(0x3200, new HandleRef(null, IntPtr.Zero), errorCode, 0, lpBuffer, lpBuffer.Capacity,
                IntPtr.Zero) != 0)
        {
            return lpBuffer.ToString();
        }
        return "Unknown Error";
    }

    private static int MakeHrFromErrorCode(int errorCode)
    {
        return (int)(0x80070000 | (uint)errorCode);
    }

    private static void WinIoError()
    {
        int errorCode = Marshal.GetLastWin32Error();
        throw new IOException(GetMessage(errorCode), MakeHrFromErrorCode(errorCode));
    }

    private void GetCommStateNative(ref Dcb lpDcb)
    {
        int commErrors = 0;
        Comstat comStat = new Comstat();

        for(int i = 0; i < CommStateRetries; i++)
        {
            if(!ClearCommError(m_Handle, ref commErrors, ref comStat))
            {
                WinIoError();
            }
            if(GetCommState(m_Handle, ref lpDcb))
            {
                break;
            }
            if(i == CommStateRetries - 1)
            {
                WinIoError();
            }
        }
    }

    private void SetCommStateNative(ref Dcb lpDcb)
    {
        int commErrors = 0;
        Comstat comStat = new Comstat();

        for(int i = 0; i < CommStateRetries; i++)
        {
            if(!ClearCommError(m_Handle, ref commErrors, ref comStat))
            {
                WinIoError();
            }
            if(SetCommState(m_Handle, ref lpDcb))
            {
                break;
            }
            if(i == CommStateRetries - 1)
            {
                WinIoError();
            }
        }
    }

    #region Nested type: COMSTAT

    [StructLayout(LayoutKind.Sequential)]
    private struct Comstat
    {
        public readonly uint Flags;
        public readonly uint cbInQue;
        public readonly uint cbOutQue;
    }

    #endregion

    #region Nested type: DCB

    [StructLayout(LayoutKind.Sequential)]
    private struct Dcb
    {
        public readonly uint DCBlength;
        public readonly uint BaudRate;
        public uint Flags;
        public readonly ushort wReserved;
        public readonly ushort XonLim;
        public readonly ushort XoffLim;
        public readonly byte ByteSize;
        public readonly byte Parity;
        public readonly byte StopBits;
        public readonly byte XonChar;
        public readonly byte XoffChar;
        public readonly byte ErrorChar;
        public readonly byte EofChar;
        public readonly byte EvtChar;
        public readonly ushort wReserved1;
    }

    #endregion

    #endregion
}

Usage of that code:

public void Connect(ConnectionParameters parameters)
{
    if(m_params != null && m_params.Equals(parameters))
        return; //already connected here

    Close();

    m_params = parameters;

    try
    {
        m_comConnection = new SerialPort();
        m_comConnection.PortName = m_params.Address;
        m_comConnection.BaudRate = m_params.BaudRate;
        m_comConnection.ReadTimeout = 6000;
        m_comConnection.WriteTimeout = 6000;
        m_comConnection.Parity = m_params.Parity;
        m_comConnection.StopBits = m_params.StopBits;
        m_comConnection.DataBits = m_params.DataBits;
        m_comConnection.Handshake = m_params.Handshake;
        m_comConnection.SafeOpen();
    }
    catch(Exception)
    {
        m_params = null;
        m_comConnection = null;
        throw;
    }
}

public void Close()
{
    if(m_params == null)
        return;

    m_comConnection.SafeClose(new Logger());
    m_comConnection = null;

    m_params = null;
}

So what am I doing wrong ? My logs couldn't look like that if the GC.SuppressFinalize() wasn't executed. What could make a Finalize() still be executed even after a GC.SuppressFinalize()?

Edit

I'm adding the code I use with my SerialPort between the opening and the closing of the port.

    public byte[] ExchangeFrame(byte[] prefix, byte[] suffix, byte[] message, int length = -1)
    {
        if(m_params == null)
            throw new NotConnectedException();

        if(length == -1)
            length = message.Length;

        Write(prefix, 0, prefix.Length);
        Write(message, 0, length);
        Write(suffix, 0, suffix.Length);

        byte[] response = new byte[length];
        byte[] prefixBuffer = new byte[prefix.Length];
        byte[] suffixBuffer = new byte[suffix.Length];


        ReadTillFull(prefixBuffer);

        while(!ByteArrayEquals(prefixBuffer, prefix))
        {
            for(int i = 0; i < prefixBuffer.Length - 1; i++)
                prefixBuffer[i] = prefixBuffer[i + 1]; //shift them all back

            if(Read(prefixBuffer, prefixBuffer.Length - 1, 1) == 0) //read one more
                throw new NotConnectedException("Received no data when reading prefix.");
        }

        ReadTillFull(response);
        ReadTillFull(suffixBuffer);

        if(!ByteArrayEquals(suffixBuffer, suffix))
            throw new InvalidDataException("Frame received matches prefix but does not match suffix. Response: " + BitConverter.ToString(prefixBuffer) + BitConverter.ToString(response) + BitConverter.ToString(suffixBuffer));

        return response;
    }

    private void ReadTillFull(byte[] buffer, int start = 0, int length = -1)
    {
        if(length == -1)
            length = buffer.Length;

        int remaining = length;

        while(remaining > 0)
        {
            int read = Read(buffer, start + length - remaining, remaining);

            if(read == 0)
                throw new NotConnectedException("Received no data when reading suffix.");

            remaining -= read;
        }
    }

    //this method looks dumb but actually, the real code has a switch statement to check if should connect by TCP or Serial
    private int Read(byte[] buffer, int start, int length)
    {
        return  m_comConnection.Read(buffer, start, length); 
    }

    private void Write(byte[] buffer, int start, int length)
    {
        m_comConnection.Write(buffer, start, length);
    }

    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    static extern int memcmp(byte[] b1, byte[] b2, long count);

    static bool ByteArrayEquals(byte[] b1, byte[] b2)
    {
        // Validate buffers are the same length.
        // This also ensures that the count does not exceed the length of either buffer.  
        return b1.Length == b2.Length && memcmp(b1, b2, b1.Length) == 0;
    }

Currently, I'm only looking for a healty communication by opening, writing, reading and closing the port. I will have to handle the case where the port is physically unplugged though.(Eventually)

Note that I'm using the ExchangeFrame method multiple times in a row (in a loop). After more testing, I found out using it once doesn't create any error. (Sending a single frame) I guess it's a problem with my code then but I am unsure of what I am doing that could make a SerialPort break when executed multiple times.

On a side note, I have another issue where I'm receiving lots of 0s in the frames I'm reading from times to times. (Didn't notice a pattern) I don't know why but I know its a new bug that appeared with this code. Not part of the question but could be related.

Edit 2

The issue was caused by myself interrupting the SerialPort's with Thread.Interrupt(). I was correctly handling the ThreadInterruptedException but it was breaking SerialPort internally. Here's the code that caused the issue:

    public void Stop()
    {
        m_run = false;

        if(m_acquisitor != null)
        {
            m_acquisitor.Interrupt();
            m_acquisitor.Join();
        }

        m_acquisitor = null;
    }

    private void Run()
    {
        while(m_run)
        {
            long time = DateTime.Now.Ticks, done = -1;
            double[] data;
            try
            {
                lock(Connection)
                {
                    if(!m_run)
                        break; //if closed while waiting for Connection

                    Connection.Connect(Device.ConnectionParams);

                    data = Acquire();
                }

                done = DateTime.Now.Ticks;
                Ping = (done - time) / TimeSpan.TicksPerMillisecond;
                Samples++;

                if(SampleReceived != null)
                    SampleReceived(this, data);

            }
            catch(ThreadInterruptedException)
            {
                continue; //checks if m_run is true, if not quits
            }

            try
            {
                if(done == -1)
                    Thread.Sleep(RETRY_DELAY); //retry delay
                else
                    Thread.Sleep((int)Math.Max(SamplingRate * 1000 - (done - time) / TimeSpan.TicksPerMillisecond, 0));
            }
            catch(ThreadInterruptedException) {} //continue
        }

        lock(Connection)
            Connection.Close(); //serialport's wrapper

    }

Giant thanks to Snoopy who helped me solve the issue in chat.

Winter
  • 3,894
  • 7
  • 24
  • 56
  • Anymore, when I'm using the serial port class I just work around the problem rather than trying to fix it directly... I guess I am a little confused, you can't just be opening and closing the port and it throws an unhandled exception in `Finalize`. Are you doing anything else in between? – Snoop Jul 21 '17 at 12:12
  • The only time I've ever seen this problem actually occur was if a cable got unplugged and then I tried to open/close the port afterwards. If that's not the case, then there is probably some small difference between your code and the code from the two posts. – Snoop Jul 21 '17 at 12:13
  • @Snoopy I'm using Read and Write methods and that's about it I think... I will post that code too. – Winter Jul 21 '17 at 12:14
  • Okay, yeah I'm just having a hard time seeing how only opening/closing the port would cause this... as I've experienced this bug and that's not what did it. Just trying to get more information to see if I can be of any help. – Snoop Jul 21 '17 at 12:15
  • @Snoopy Thank you for your interest, added more information. – Winter Jul 21 '17 at 12:27
  • Is there any way you could go back to using the serial port *as-is* instead of using the *ObjectNotDisposed* exception code? I switched back to doing it the normal way as the bug fix introduced more problems I do not care to elaborate on for right now. I think that in all cases except for physically unplugging the port you should never have that problem. – Snoop Jul 21 '17 at 12:37
  • The other thing is, I don't ever dispose the serial ports. They are managed pretty well by the Windows OS... I don't think that letting the ports go on un-disposed will lead to any serious memory leaks or anything... – Snoop Jul 21 '17 at 12:39
  • @Snoopy 1) I don't get what you mean by "using the ObjectNotDisposed", It's not an exception I'm throwing by myself. Do you mean stop handling it or do you mean to stop using the SerialPortFixer ? 2) It would be pretty annoying to have to close the application from a computer everytime you want to use it on another computer. Closing my `SerialPort`s cleanly lets people in the lab work faster an encounter less annoying issues. – Winter Jul 21 '17 at 12:41
  • Yeah... so that serial port fixer code properly shuts down the event-loop runner. What I am saying is that 99.999% of the time the event-loop runner will get shut down properly, so you won't get that weird problem. So I'm suggesting that you remove the *SerialPortFixer* and just go back to using the serial port in the normal way. – Snoop Jul 21 '17 at 12:43
  • As I added in the question, no issue appear when sending a single frame, I will test immediately what happen when I unplugging it when using it for 1-frame messages. The more I think about it, the more it feels like I'm doing something wrong in the `ExchangeFrame` method. But the original question still applies, how the **** can Finalize be called after GC.SuppressFinalize ... I will try removing SerialPortFixer but as far as I remember, the issue didn't started after adding it. – Winter Jul 21 '17 at 12:45
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/149810/discussion-between-snoopy-and-winter). – Snoop Jul 21 '17 at 12:46

0 Answers0