10

COM ポート経由で USB ドングルと通信する C# Windows フォーム アプリケーションがあります。.Net 2.0 で SerialPort クラスを通信に使用しています。シリアル ポート オブジェクトは、アプリケーションの存続期間中開いています。アプリケーションはデバイスにコマンドを送信し、デバイスから非送信請求データを受信することもできます。

私の問題は、フォームが閉じられたときに発生します。COM ポートを閉じようとすると、(残念ながらランダムに) ObjectDisposedException が発生します。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 のためのものであり、問​​題は解決しませんでした。

私の Close() コードは次のとおりです。

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

私のロギングは、SerialPort の BaseStream (これは最初のtryブロックにあります) を閉じようとしても実行されないことを示したので、この行を削除して実験しましたが、例外はまだ定期的にスローされます - 2 番目のtryブロックのロギングが表示され、例外が発生しました。どちらの 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();
                }
            }
        }
    }    
}
4

3 に答える 3

31

注:現在の調査結果は、Windows7の.NETFramework 4.0 32ビットでのみテストされています。他のバージョンで機能する場合は、コメントしてください。

編集: TL; DR:これが回避策の核心です。説明については、以下を参照してください。SerialPortを開くときも、SerialPortFixer使用することを忘れないでください。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クラスにはさまざまなバグがあり(これまで見てきました)、Web上のすべての問題につながります。

  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(port); GC.SuppressFinalize(port.BaseStream);

シリアルポートをラップし、これらすべての問題を修正するクラスは次のとおりです:http: //pastebin.com/KmKEVzR8

この回避策クラスでは、.NET 1.1の未処理の例外動作に戻す必要はなく、優れた安定性で動作します。

これが私の最初の貢献ですので、私が正しくやっていないのなら失礼します。私はそれが誰かを助けることを願っています。

于 2012-01-29T21:58:11.627 に答える
12

はい、SerialPort クラスには、この種のクラッシュを可能にする欠陥があります。Open() を呼び出すと、SerialPort はスレッドを開始します。そのスレッドはポート上のイベントを監視します。たとえば、DataReceived イベントを取得する方法です。BaseStream.Close() または Close() または Dispose() メソッドを呼び出すと (これらはすべて同じことを行います)、SerialPortはスレッドに終了を要求するだけで、終了するのを待ちません。

これはあらゆる種類の問題を引き起こします。文書化されたものは、ポートを閉じた直後に Open() することは想定されていません。しかし、ここで問題になるのは、Close() 呼び出しの直後にプログラムが終了するか、ガベージ コレクションが発生した場合です。これにより、ファイナライザーが実行され、ハンドルも閉じようとします。ワーカー スレッドがまだ使用しているため、まだ開いています。スレッド レースが可能になりました。これは適切に連動していません。kaboom は、ファイナライザー スレッドが同じことをしようとする直前にワーカーがハンドルを閉じて終了したときに発生します。例外はファイナライザー スレッドで発生するため、キャッチできません。CLR はプログラムを中止します。

2.0 以降の .NET のすべてのバージョンでは、SerialPort の問題を回避するために、クラスに小さな変更が加えられました。まだ .NET 2.0 を使用している場合の最善の方法は、実際には Close() を呼び出さないことです。とにかく自動的に発生し、ファイナライザーが処理します。何らかの理由 (ハード クラッシュまたはプログラムの中止) でそれが発生しない場合でも、Windows はポートが閉じていることを確認します。

于 2012-01-19T17:26:21.487 に答える
7

これはかなり古いですが、現在の質問であることは知っています。最近この問題が発生し、解決策を探したところ、リリース ノートによると、この問題は最終的に .NET Framework 4.7 で修正されたようです。 https://github.com/Microsoft/dotnet/blob/master/releases/net47/dotnet47-changes.md

実行中にデバイスのプラグを抜くと、SerialStream クラスでメモリ リークが発生する可能性がある SerialPort の問題を修正しました。[288363]

于 2017-09-27T07:32:39.607 に答える