ObjectDisposedException при закрытии SerialPort в .Net 2.0 - PullRequest
10 голосов
/ 19 января 2012

У меня есть приложение C # Windows Forms, которое связывается с USB-ключом через COM-порт. Я использую класс SerialPort в .Net 2.0 для связи, и объект последовательного порта открыт в течение всего жизненного цикла приложения. Приложение отправляет команды на устройство, а также может получать незапрошенные данные с устройства.

Моя проблема возникает, когда форма закрыта - я получаю (случайно, к сожалению) исключение ObjectDisposedException при попытке закрыть COM-порт. Вот трассировка стека 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: 

Я нашел сообщения от людей с похожими проблемами и попробовал обходной путь [здесь] [1]

[1]: http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html хотя это для IOException и не остановило проблему.

Мой код закрытия () выглядит следующим образом:

        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 + "*******");
        }            
    }

Моя регистрация показала, что выполнение никогда не выходило за пределы попытки закрыть BaseStream SerialPort (это находится в первом try блоке), поэтому я экспериментировал с удалением этой строки, но исключение по-прежнему периодически генерируется - регистрация во второй * Блок 1016 * появился, тогда произошло исключение. Ни один блок catch не перехватывает исключение.

Есть идеи?

ОБНОВЛЕНИЕ - добавление полного класса:

    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 ]

31 голосов
/ 30 января 2012

Примечание. Текущие результаты были протестированы только на 32-разрядной платформе .NET Framework 4.0 в Windows 7, поэтому не стесняйтесь комментировать, если она работает в других версиях.

Редактировать: TL; DR: Вот суть обходного пути. Смотрите объяснение ниже. Не забудьте также использовать SerialPortFixer при открытии SerialPort. ILog из 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);
    }
}

Я боролся с этим пару дней в недавнем проекте.

В классе .NET SerialPort есть много разных ошибок (которые я видел до сих пор), которые приводят ко всем головным болям в сети.

  1. Отсутствующий флаг структуры DCB здесь: http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html Это исправлено классом SerialPortFixer, за это идут авторы.

  2. При удалении последовательного USB-устройства при закрытии SerialPortStream eventLoopRunner запрашивается остановка, а SerialPort.IsOpen возвращает false. После удаления это свойство проверяется, и закрытие внутреннего последовательного потока пропускается, таким образом сохраняя исходный дескриптор открытым бесконечно (до тех пор, пока не завершится финализатор, что приведет к следующей проблеме).

    Решением для этого является ручное закрытие внутреннего последовательного потока. Мы можем получить его ссылку с помощью SerialPort.BaseStream до того, как произошло исключение, или путем отражения и получение поля «internalSerialStream».

  3. При удалении последовательного USB-устройства закрытие внутреннего последовательного потока вызывает исключение и закрывает внутренний дескриптор, не дожидаясь завершения его потока eventLoopRunner, вызывая позднее невыбираемое исключение ObjectDisposedException из потока фонового цикла цикла событий, когда запускается финализатор потока (который, как ни странно, не выдает исключение, но все еще не может ожидать eventLoopRunner).

    Симптом этого здесь: https://connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port

    Решение состоит в том, чтобы вручную попросить обработчик цикла событий остановиться (через отражение) и дождаться его завершения, прежде чем закрывать внутренний последовательный поток.

  4. Поскольку команда Dispose генерирует исключения, финализатор не подавляется. Это легко решаемо:

    GC.SuppressFinalize (порт); GC.SuppressFinalize (port.BaseStream);

Вот класс, который оборачивает последовательный порт и исправляет все эти проблемы: http://pastebin.com/KmKEVzR8

С этим классом обходного пути возвращение к необработанному поведению исключений .NET 1.1 не требуется, и оно работает с превосходной стабильностью.

Это мой первый вклад, поэтому, пожалуйста, извините, если я не делаю это правильно. Надеюсь, это кому-нибудь поможет.

12 голосов
/ 19 января 2012

Да, в классе SerialPort есть недостаток, который делает возможным такой сбой. SerialPort запускает поток, когда вы вызываете Open (). Этот поток отслеживает события в порту, например, вы получаете событие DataReceived. Когда вы вызываете метод BaseStream.Close () или Close () или Dispose () (все они делают одно и то же), тогда SerialPort только запрашивает поток на выходе, но не ожидает его выхода.

Это вызывает всевозможные проблемы. Один документированный, вы не должны открывать () порт сразу после его закрытия. Но беда здесь в том, что ваша программа закрывается или мусор собирается сразу после вызова Close (). Это запускает финализатор и пытается также закрыть дескриптор. Это все еще открыто, потому что рабочий поток все еще использует это. Гонка потоков теперь возможна, она не блокируется должным образом. Kaboom происходит, когда работнику удалось закрыть дескриптор и выйти непосредственно перед тем, как поток финализатора попытается сделать то же самое. Исключение невозможно отследить, потому что это происходит в потоке финализатора, CLR прерывает программу.

В каждой версии .NET начиная с 2.0 были небольшие изменения в классах, чтобы обойти проблемы SerialPort. Безусловно, лучшее, что нужно сделать, если вы все еще используете .NET 2.0, - это , а не , фактически вызывать Close (). В любом случае это происходит автоматически, финализатор позаботится об этом. Даже если этого не произойдет по какой-либо причине (сбой или прерывание работы программы), Windows гарантирует, что порт закрыт.

4 голосов
/ 27 сентября 2017

Я знаю, что это довольно старый, но актуальный вопрос. У меня была эта проблема недавно, и после поиска решения, похоже, что эта проблема, наконец, исправлена ​​в .NET Framework 4.7, согласно примечаниям к выпуску. https://github.com/Microsoft/dotnet/blob/master/releases/net47/dotnet47-changes.md

Исправлена ​​проблема в SerialPort, когда отключение устройства во время выполнения могло вызвать утечку памяти в классе SerialStream. [288363]

...