2012-01-19 97 views
8

我有一個C#windows窗體應用程序,它通過COM端口與USB加密狗進行通信。我使用.Net 2.0中的SerialPort類進行通信,並且串行端口對象在應用程序的整個生命週期中都處於打開狀態。應用程序向設備發送命令,也可以從設備接收未經請求的數據。在.Net 2.0中關閉SerialPort時發生ObjectDisposedException異常

當窗體關閉時發生我的問題 - 當嘗試關閉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 + "*******"); 
     }    
    } 

我的記錄顯示,執行從來沒有超越試圖關閉一個串口的BaseStream(這是第一try塊),所以我刪除此試驗但異常仍然是週期性拋出 - 第二個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(); 
       } 
      } 
     } 
    }  
} 
+0

你似乎是「泄漏」(不關閉或處置)的'SerialStream'類的實例(因爲'SerialStream.Finalize'被稱爲在你的堆棧跟蹤),我建議,這是*一個*問題,但是要確定這與您目前的問題有什麼關係需要更多信息。 –

+0

感謝您的回覆。哪些信息將有助於查明問題? – barry

+0

包含上面的'Close'方法的整個類將有所幫助。 –

回答

28

注意:目前的研究結果只在Windows 7上的.NET Framework 4.0 32位上進行過測試,請在其他版本上發表評論。

編輯: TL; DR: 以下是解決方法的癥結所在。請參閱下面的解釋。 打開串口時也不要忘記使用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類有很多不同的錯誤(至今我見過),導致網絡上所有的麻煩。

  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未處理的異常行爲是不必要的,它具有優良的穩定性工作。

這是我的第一個貢獻,所以請原諒,如果我沒有做對。我希望它能幫助別人。

+0

看起來這已經解決了這個問題。我做的唯一改變是最後一個while語句:while(!eventLoopEndedWaitHandle.WaitOne(2000,false));'。 .Net 2.0沒有沒有布爾參數的方法。 – barry

+1

更新:在.NET 4.5上,似乎MS已經修復了崩潰問題,但仍然處理時間過長(可能正在等待終結器),因此此解決方案仍然有效並仍然有效(在Win7和Win8 64-位.NET 4.5)。 –

+0

我仍然在4.5的崩潰,但答案的修復完美。一個恥辱,我只能給一個upvote。 –

12

是的,有在SerialPort類,使這種碰撞可能的缺陷。當您調用Open()時,SerialPort啓動一個線程。該線程監視端口上的事件,例如,您將如何獲取DataReceived事件。當你調用BaseStream.Close()或Close()或Dispose()方法時(它們都做同樣的事情),那麼SerialPort只有要求線程退出,但不會等待它退出。

這會導致各種問題。一個文件記錄了一個,你不應該在關閉它之後立即打開()一個端口。但是這裏的錯誤是當你的程序退出或在Close()調用之後立即收集垃圾時。這運行終結器,它也試圖關閉句柄。它仍然是打開的,因爲工作線程仍在使用它。現在有可能進行穿線競賽,但這並沒有正確聯鎖。 kaboom發生在工作人員設法關閉句柄並在終結器線程嘗試執行相同操作之前退出時。異常是無法捕捉的,因爲它發生在終結器線程中,CLR中止程序。

自2.0以來,.NET的每個版本都在類中進行了少量更改以解決SerialPort問題。到目前爲止,如果你仍然使用.NET 2.0,最好的辦法是而不是實際上調用Close()。無論如何,它會自動發生,終結者會照顧它。即使由於某種原因(硬件崩潰或程序中止)沒有發生,Windows仍然會確保該端口關閉。

+0

感謝Hans,這非常有幫助。我已經添加了完整的課程,它幫助任何人查明問題,但我懷疑我只是犯了這個.net錯誤。我會研究不調用'Close()',看看我如何繼續。 – barry

+1

@Hans:有沒有什麼好的方法可以關閉串口,以防其它程序可以使用它,或者處理USB串口適配器意外拔出的可能性?我最終使用的方法是編寫一個程序,其目的是打開一個串口並將數據傳遞給主程序。如果有必要關閉該端口或如果它死了,那麼該程序就會死掉而不會影響主程序。雖然這看起來很。 – supercat

相關問題