在GC.SuppressFinalize()之后仍然调用SerialPort的Finalize()

时间:2017-07-20 20:07:52

标签: c# .net serial-port finalize

我真的很难在C#中使用SerialPort,并发现自2010年以来,.NET中串行端口的API存在漏洞。我已将{{3}中的补丁组合在一起}和this blog post

但是,当我尝试关闭我的串口时,我仍然在GC线程中收到一个未处理的异常,导致程序崩溃。有趣的是,堆栈跟踪的顶部恰好是SerialStream Finalize()方法,即使SerialStream对象已经是GC.SuppressFinalize()的答案代码我联系了。

这是例外:

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()

这里是串口调试的日志:

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

这是我的代码,我发布的2个链接的略微修改版本:

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
}

使用该代码:

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;
}

那么我做错了什么?如果GC.SuppressFinalize()未执行,我的日志看起来就不合适了。什么可以使Finalize()仍然在GC.SuppressFinalize()之后仍然执行?

修改

我在端口的开启和关闭之间添加了我用SerialPort的代码。

    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;
    }

目前,我只是通过打开,写入,阅读和关闭端口来寻找健康的通信。我将不得不处理端口物理拔出的情况。(最终)

请注意,我连续多次使用ExchangeFrame方法(在循环中)。经过更多测试后,我发现一旦没有产生任何错误就使用它。 (发送一个帧)我想我的代码存在问题,但我不确定我在做什么,多次执行会导致SerialPort中断。

另一方面,我还有另一个问题,即我在不时阅读的框架中收到大量的0。 (没有注意到一种模式)我不知道为什么,但我知道这个代码出现了一个新的错误。不是问题的一部分,但可能是相关的。

编辑2

问题原因是我自己用Thread.Interrupt()打断了SerialPort。我正确地处理了ThreadInterruptedException,但它在内部破坏了SerialPort。以下是导致此问题的代码:

    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

    }

非常感谢that answer帮助我在聊天中解决问题。

0 个答案:

没有答案