9


6

ObjectDisposedException lors de la fermeture de SerialPort dans .Net 2.0

J’ai une application Windows Forms C # qui communique avec un dongle USB via un port COM. J’utilise la classe SerialPort dans .Net 2.0 pour la communication, et l’objet de port série est ouvert pour la durée de vie de l’application. L’application envoie des commandes à l’appareil et peut également recevoir des données non sollicitées de l’appareil.

Mon problème se produit lorsque le formulaire est fermé - j’obtiens (au hasard, malheureusement) une ObjectDisposedException lorsque je tente de fermer le port COM. Voici la trace de la pile Windows:

System.ObjectDisposedException was unhandled


Message=Safe handle has been closed
  Source=System
  ObjectName=""
  StackTrace:
       at Microsoft.Win32.UnsafeNativeMethods.SetCommMask(SafeFileHandle hFile, Int32 dwEvtMask)
       at System.IO.Ports.SerialStream.Dispose(Boolean disposing)
       at System.IO.Ports.SerialStream.Finalize()
  InnerException:

J’ai trouvé des messages de personnes ayant des problèmes similaires et j’ai essayé la solution [ici] [1]

[1]: http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html bien que ce soit pour une IOException et n’a pas arrêté le problème.

Mon code Close () est le suivant:

        public void Close()
    {
        try
        {
            Console.WriteLine("******ComPort.Close - baseStream.Close*******");
            baseStream.Close();
        }
        catch (Exception ex)
        {
            Console.WriteLine("******ComPort.Close baseStream.Close raised exception: " + ex + "*******");
        }
        try
        {
            _onDataReceived = null;
            Console.WriteLine("******ComPort.Close - _serialPort.Close*******");
            _serialPort.Close();
        }
        catch (Exception ex)
        {
            Console.WriteLine("******ComPort.Close - _serialPort.Close raised exception: " + ex + "*******");
        }
    }

Ma journalisation a montré que l’exécution n’allait jamais au-delà de la tentative de fermeture du BaseStream du SerialPort (c’est dans le premier bloc try), j’ai donc expérimenté la suppression de cette ligne mais l’exception est toujours levée périodiquement - la journalisation dans le deuxième bloc` try` est apparu alors l’exception s’est produite. Aucun bloc catch n’attrape l’exception.

Des idées?

MISE À JOUR - ajout d’une classe complète:

    namespace My.Utilities
{
    public interface ISerialPortObserver
    {
        void SerialPortWriteException();
    }

    internal class ComPort : ISerialPort
    {
        private readonly ISerialPortObserver _observer;
        readonly SerialPort _serialPort;

        private DataReceivedDelegate _onDataReceived;
        public event DataReceivedDelegate OnDataReceived
        {
            add { lock (_dataReceivedLocker) { _onDataReceived += value; } }
            remove { lock (_dataReceivedLocker) { _onDataReceived -= value; } }
        }

        private readonly object _dataReceivedLocker = new object();
        private readonly object _locker = new object();

        internal ComPort()
        {
            _serialPort = new SerialPort { ReadTimeout = 10, WriteTimeout = 100, DtrEnable = true };
            _serialPort.DataReceived += DataReceived;
        }

        internal ComPort(ISerialPortObserver observer) : this()
        {
            _observer = observer;
        }

        private void DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            DataReceivedDelegate temp = null;

            lock (_locker)
            {
                lock (_dataReceivedLocker)
                {
                    temp = _onDataReceived;
                }

                string dataReceived = string.Empty;
                var sp = (SerialPort) sender;

                try
                {
                    dataReceived = sp.ReadExisting();
                }
                catch (Exception ex)
                {
                    Logger.Log(TraceLevel.Error, "ComPort.DataReceived raised exception: " + ex);
                }

                if (null != temp && string.Empty != dataReceived)
                {
                    try
                    {
                        temp(dataReceived, TickProvider.GetTickCount());
                    }
                    catch (Exception ex)
                    {
                        Logger.Log(TraceLevel.Error, "ComPort.DataReceived raised exception calling handler: " + ex);
                    }
                }
            }
        }

        public string Port
        {
            set
            {
                try
                {
                    _serialPort.PortName = value;
                }
                catch (Exception ex)
                {
                    Logger.Log(TraceLevel.Error, "ComPort.Port raised exception: " + ex);
                }
            }
        }

        private System.IO.Stream comPortStream = null;
        public bool Open()
        {
            SetupSerialPortWithWorkaround();
            try
            {
                _serialPort.Open();
                comPortStream = _serialPort.BaseStream;
                return true;
            }
            catch (Exception ex)
            {
                Logger.Log(TraceLevel.Warning, "ComPort.Open raised exception: " + ex);
                return false;
            }
        }

        public bool IsOpen
        {
            get
            {
                SetupSerialPortWithWorkaround();
                try
                {
                    return _serialPort.IsOpen;
                }
                catch(Exception ex)
                {
                    Logger.Log(TraceLevel.Error, "ComPort.IsOpen raised exception: " + ex);
                }

                return false;
            }
        }

        internal virtual void SetupSerialPortWithWorkaround()
        {
            try
            {
                //http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html
                // This class is meant to fix the problem in .Net that is causing the ObjectDisposedException.
                SerialPortFixer.Execute(_serialPort.PortName);
            }
            catch (Exception e)
            {
                Logger.Log(TraceLevel.Info, "Work around for .Net SerialPort object disposed exception failed with : " + e + " Will still attempt open port as normal");
            }
        }

        public void Close()
        {
            try
            {
                comPortStream.Close();
            }
            catch (Exception ex)
            {
                Logger.Log(TraceLevel.Error, "ComPortStream.Close raised exception: " + ex);
            }
            try
            {
                _onDataReceived = null;
                _serialPort.Close();
            }
            catch (Exception ex)
            {
                Logger.Log(TraceLevel.Error, "ComPort.Close raised exception: " + ex);
            }
        }

        public void WriteData(string aData, DataReceivedDelegate handler)
        {
            try
            {
                OnDataReceived += handler;
                _serialPort.Write(aData + "\r\n");
            }
            catch (Exception ex)
            {
                Logger.Log(TraceLevel.Error, "ComPort.WriteData raised exception: " + ex);

                if (null != _observer)
                {
                    _observer.SerialPortWriteException();
                }
            }
        }
    }
}

3 Answer


30


  • Remarque: Les résultats actuels n’ont été testés que sur .NET Framework 4.0 32 bits sous Windows 7, n’hésitez pas à commenter si cela fonctionne sur d’autres versions. *

  • Edit: * TL; DR: Voici le nœud de la solution de contournement. Voir ci-dessous pour l’explication. N’oubliez pas d’utiliser SerialPortFixer lors de l’ouverture du SerialPort également. ILog provient de log4net.

static readonly ILog s_Log = LogManager.GetType("SerialWorkaroundLogger");

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

J’ai lutté avec cela pendant quelques jours dans un projet récent.

Il existe de nombreux bogues différents (que j’ai vus jusqu’à présent) avec la classe .NET SerialPort qui conduisent à tous les maux de tête sur le Web.

  1. Le drapeau struct DCB manquant ici: http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html Celui-ci est corrigé par la classe SerialPortFixer, les crédits vont à l’auteur pour celui-là.

  2. Lorsqu’un périphérique série USB est retiré, lors de la fermeture du SerialPortStream, EventLoopRunner est invité à s’arrêter et SerialPort.IsOpen renvoie false. Lors de l’élimination, cette propriété est vérifiée et la fermeture du flux série interne est ignorée, gardant ainsi la poignée d’origine ouverte indéfiniment (jusqu’à ce que le finaliseur fonctionne, ce qui conduit au problème suivant). + La solution pour celui-ci est de fermer manuellement le flux série interne. Nous pouvons obtenir sa référence par SerialPort.BaseStream avant que l’exception ne se produise ou par réflexion et obtention du champ "internalSerialStream".

  3. Lorsqu’un périphérique série USB est retiré, la fermeture du port série interne stream lève une exception et ferme le handle interne sans attendre la fin de son thread eventLoopRunner, provoquant une ObjectDisposedException non capturable du thread du runner de boucle d’événement d’arrière-plan lorsque le finaliseur du flux s’exécute (ce qui évite étrangement de lever l’exception mais ne parvient toujours pas à attendre le eventLoopRunner). + Symptôme de cela ici: https://connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port + La solution consiste à demander manuellement au coureur de boucle d’événements s’arrêter (via la réflexion) et attendre qu’il se termine avant de fermer le flux série interne.

  4. Puisque Dispose lève des exceptions, le finaliseur n’est pas supprimé. This est facilement résoluble: + GC.SuppressFinalize (port); GC.SuppressFinalize (port.BaseStream);

Avec cette classe de contournement, le retour au comportement d’exception non géré .NET 1.1 n’est pas nécessaire et fonctionne avec une excellente stabilité.

Ceci est ma première contribution, alors veuillez m’excuser si je ne le fais pas correctement. J’espère que ça aide quelqu’un.


12


Oui, il y a une faille dans la classe SerialPort qui rend possible ce genre de plantage. SerialPort démarre un thread lorsque vous appelez Open (). Ce thread surveille les événements sur le port, c’est ainsi que vous obtenez l’événement DataReceived par exemple. Lorsque vous appelez la méthode BaseStream.Close () ou Close () ou Dispose () (ils font tous la même chose), SerialPort ne file que le thread pour quitter mais n’attend pas qu’il se termine.

Cela provoque toutes sortes de problèmes. Un documenté, vous n’êtes pas censé ouvrir () un port juste après l’avoir fermé. Mais l’incident se produit ici lorsque votre programme se ferme ou que les ordures sont récupérées juste après l’appel Close (). Cela exécute le finaliseur et essaie également de fermer la poignée. Il est toujours ouvert car le thread de travail l’utilise toujours. Une course de filetage est désormais possible, elle n’est pas correctement enclenchée. Le kaboom se produit lorsque le travailleur a réussi à fermer la poignée et à quitter juste avant que le thread du finaliseur essaie de faire de même. L’exception est inaccessible car elle se produit dans le thread du finaliseur, le CLR abandonne le programme.

Chaque version de .NET depuis 2.0 a eu de petites modifications dans les classes pour contourner les problèmes SerialPort. De loin, la meilleure chose à faire si vous êtes toujours sur .NET 2.0 est de pas appeler réellement Close (). Cela se passe quand même automatiquement, le finaliseur s’en charge. Même si cela ne se produit pas pour une raison quelconque (plantage dur ou abandon de programme), Windows s’assure que le port est fermé.


3


Je sais que c’est une question assez ancienne, mais actuelle. J’ai eu ce problème récemment et après avoir cherché une solution, il semble que ce problème soit finalement résolu avec .NET Framework 4.7, selon les notes de publication. https://github.com/Microsoft/dotnet/blob/master/releases/net47/dotnet47-changes.md

_ Correction d’un problème dans SerialPort où le débranchement du périphérique pendant l’exécution pouvait provoquer une fuite de mémoire dans la classe SerialStream. [288363] _