SerialPort's Finalize () todavía se llama después de GC.SuppressFinalize ()

Estoy intentando realmente usar unSerialPort en C # y descubrí que la API para puertos serie en .NET tiene errores desde 2010. He combinado los parches realizados enesta entrada de blog yesa respuesta.

Sin embargo, sigo recibiendo una excepción no controlada en el subproceso GC que bloquea mi programa cuando intento cerrar mi puerto serie. Divertidamente, la parte superior de la traza de la pila está exactamente enSerialStream'sFinalize() método incluso si elSerialStream objeto ya ha sidoGC.SuppressFinalize() por el código de la respuesta que vinculé.

Aquí está la excepción:

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

Aquí están los registros para la depuración del puerto serie:

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

Aquí está mi código, una versión ligeramente modificada de los 2 enlaces que publiqué:

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.dl,l", 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
}

Uso de ese código:

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

Entonces, ¿qué estoy haciendo mal? Mis registros no podrían verse así si elGC.SuppressFinalize() No fue ejecutado. ¿Qué podría hacer unFinalize() todavía se ejecutará incluso después de unGC.SuppressFinalize()?

Editar

Estoy agregando el código que uso con miSerialPort entre la apertura y el cierre del puerto.

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

Actualmente, solo estoy buscando una comunicación saludable al abrir, escribir, leer y cerrar el puerto. Sin embargo, tendré que manejar el caso donde el puerto está físicamente desconectado. (Eventualmente)

Tenga en cuenta que estoy usando elExchangeFrame método varias veces seguidas (en un bucle). Después de más pruebas, descubrí que usarlo una vez no crea ningún error. (Enviando un solo cuadro) Supongo que es un problema con mi código, pero no estoy seguro de lo que estoy haciendo que podríaSerialPort romper cuando se ejecuta varias veces.

En una nota al margen, tengo otro problema en el que recibo muchos ceros en los marcos que leo de vez en cuando. (No noté un patrón) No sé por qué, pero sé que es un nuevo error que apareció con este código. No es parte de la pregunta pero podría estar relacionado.

Editar 2

El problema fue causado por mí mismo interrumpiendo el SerialPort conThread.Interrupt(). Estaba manejando correctamente elThreadInterruptedException pero estaba rompiendo SerialPort internamente. Aquí está el código que causó el problema:

    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

    }

Gigante gracias aSnoopy quien me ayudó a resolver el problema en el chat.

Respuestas a la pregunta(0)

Su respuesta a la pregunta