2012-01-19 30 views
8

Tôi có một ứng dụng biểu mẫu C# windows giao tiếp với USB dongle qua cổng COM. Tôi đang sử dụng lớp SerialPort trong .Net 2.0 để giao tiếp, và đối tượng cổng nối tiếp mở cho toàn bộ thời gian của ứng dụng. Ứng dụng gửi lệnh đến thiết bị và cũng có thể nhận dữ liệu không mong muốn từ thiết bị.ObjectDisposedException khi đóng SerialPort trong .Net 2.0

Sự cố của tôi xảy ra khi biểu mẫu bị đóng - tôi nhận được (ngẫu nhiên, không may) một ObjectDisposedException khi cố gắng đóng cổng COM. Dưới đây là Windows stack trace:

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: 

Tôi đã tìm thấy bài viết từ những người có vấn đề tương tự và đã cố gắng thực hiện giải pháp [ở đây] [1]

[1]: http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html mặc dù đó là cho một IOException và đã không dừng vấn đề.

Close() Mã của tôi là như sau:

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

đăng nhập của tôi cho thấy thực hiện mà không bao giờ nhận được vượt quá cố gắng để đóng BaseStream của SerialPort (đây là trong try khối đầu tiên), vì vậy tôi đã thử nghiệm với loại bỏ này dòng nhưng ngoại lệ vẫn được ném định kỳ - việc ghi nhật ký trong khối try thứ hai xuất hiện sau đó ngoại lệ đã xảy ra. Không bắt khối bắt ngoại lệ.

Bất kỳ ý tưởng nào?

CẬP NHẬT - thêm lớp đầy đủ:

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

Dường như bạn đang bị 'rò rỉ' (không đóng hoặc hủy) phiên bản của lớp 'SerialStream' (vì' SerialStream.Finalize' được gọi trong dấu vết ngăn xếp của bạn), tôi đề nghị rằng đây là * a * vấn đề, tuy nhiên để xác định những gì mối quan hệ này mang đến vấn đề hiện tại của bạn thêm thông tin là cần thiết. –

+0

Cảm ơn bạn đã phản hồi. Thông tin nào sẽ giúp xác định vấn đề? – barry

+0

Toàn bộ lớp có chứa phương thức 'Close' ở trên sẽ giúp ích. –

Trả lời

28

Lưu ý: Các kết quả hiện tại chỉ được thử nghiệm trên .NET Framework 4.0 32 bit trên Windows 7, vui lòng bình luận nếu nó hoạt động trên các phiên bản khác.

Chỉnh sửa: TL; DR: Đây là mấu chốt của giải pháp thay thế. Xem dưới đây để được giải thích. Đừng quên sử dụng SerialPortFixer khi mở SerialPort. ILog là từ 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); 
    } 
} 

Tôi đã vật lộn với điều này trong một vài ngày trong một dự án gần đây.

Có nhiều lỗi khác nhau (tôi đã thấy cho đến nay) với lớp .NET SerialPort dẫn đến tất cả các cơn đau đầu trên web.

  1. Các thiếu DCB struct cờ ở đây: http://zachsaw.blogspot.com/2010/07/net-serialport-woes.html Cái này là cố định bởi lớp SerialPortFixer, tín dụng đi cho tác giả cho rằng một trong.

  2. Khi thiết bị nối tiếp USB bị xóa, khi đóng SerialPortStream, eventLoopRunner được yêu cầu dừng và SerialPort.IsOpen trả về false. Khi xử lý tài sản này được kiểm tra và đóng dòng nối tiếp nội bộ bị bỏ qua, do đó giữ tay cầm ban đầu mở vô thời hạn (cho đến khi finalizer chạy dẫn đến vấn đề tiếp theo).

    Giải pháp cho việc này là tự đóng luồng nối tiếp nội bộ. Chúng ta có thể lấy tham chiếu của nó bằng SerialPort.BaseStream trước khi ngoại lệ đã xảy ra hoặc bằng cách phản chiếu và nhận được trường "internalSerialStream".

  3. Khi thiết bị nối tiếp USB bị xóa, đóng dòng nối tiếp bên trong sẽ ném ngoại lệ và đóng bộ điều khiển bên trong mà không đợi chuỗi eventLoopRunner kết thúc, gây ra một ObjectDisposedException không thể tách rời khỏi chuỗi sự kiện nền sau này. trình chạy cuối cùng của luồng chạy (điều này sẽ tránh được ngoại lệ một cách kỳ quặc nhưng vẫn không đợi được eventLoopRunner).

    Triệu chứng về điều đó ở đây: https://connect.microsoft.com/VisualStudio/feedback/details/140018/serialport-crashes-after-disconnect-of-usb-com-port

    Giải pháp là tự hỏi người chạy vòng lặp sự kiện để stop (thông qua phản ánh) và chờ đợi cho nó để kết thúc trước khi đóng cửa suối nối tiếp nội bộ.

  4. Vì Vứt bỏ ném ngoại lệ, trình kết thúc không bị chặn. Điều này có thể dễ dàng giải được:

    GC.SuppressFinalize (port); GC.SuppressFinalize (port.BaseStream);

Dưới đây là một lớp bọc lấy một cổng nối tiếp và sửa chữa tất cả các vấn đề này: http://pastebin.com/KmKEVzR8

Với lớp workaround này, quay trở lại NET 1.1 Hành vi ngoại lệ unhandled là không cần thiết và nó hoạt động với sự ổn định tuyệt vời .

Đây là đóng góp đầu tiên của tôi, vì vậy hãy tha lỗi cho tôi nếu tôi không làm đúng. Tôi hi vọng nó giúp ích cho ai đó.

+0

Có vẻ như điều này đã giải quyết được vấn đề. Thay đổi duy nhất tôi thực hiện là câu lệnh 'while' cuối cùng:' while (! EventLoopEndedWaitHandle.WaitOne (2000, false)); '. .Net 2.0 không có phương thức mà không có tham số boolean. – barry

+1

Cập nhật: trên .NET 4.5 rõ ràng vụ tai nạn dường như đã được sửa bởi MS, nhưng vẫn còn xử lý vẫn mở quá lâu (có thể chờ đợi finalizer), vì vậy giải pháp này vẫn còn hợp lệ và vẫn hoạt động (thử nghiệm trên Win7 và Win8 64- bit .NET 4.5). –

+0

Tôi vẫn gặp sự cố trên 4.5 nhưng bản sửa lỗi từ câu trả lời hoạt động hoàn hảo. Một sự xấu hổ tôi chỉ có thể cung cấp cho một upvote. –

12

Vâng, có một lỗ hổng trong lớp SerialPort mà làm cho loại tai nạn có thể. SerialPort khởi động một chuỗi khi bạn gọi Open(). Đó là chủ đề đồng hồ cho các sự kiện trên cổng, đó là cách bạn nhận được sự kiện DataReceived chẳng hạn. Khi bạn gọi phương thức BaseStream.Close() hoặc Close() hoặc Dispose() (tất cả chúng đều làm tương tự) thì SerialPort chỉ yêu cầu chuỗi để thoát nhưng không chờ nó thoát.

Điều này gây ra tất cả các loại sự cố. Một tài liệu một, bạn không phải mở() một cổng ngay sau khi đóng nó. Nhưng rủi ro ở đây là khi chương trình của bạn thoát ra hoặc rác thu thập ngay sau cuộc gọi Close(). Điều đó chạy finalizer và nó cố gắng để đóng xử lý là tốt. Nó vẫn mở vì chuỗi công nhân vẫn đang sử dụng nó. Một cuộc đua threading là bây giờ có thể, điều này không phải là interlocked đúng cách. Các kaboom xảy ra khi công nhân quản lý để đóng xử lý và thoát ngay trước khi thread finalizer cố gắng làm như vậy. Ngoại lệ là uncatchable vì nó xảy ra trong thread finalizer, CLR hủy bỏ chương trình.

Mọi phiên bản .NET kể từ 2.0 đều có những thay đổi nhỏ trong các lớp để khắc phục sự cố SerialPort. Cho đến nay, điều tốt nhất cần làm nếu bạn vẫn còn trên .NET 2.0 là không phải thực sự gọi Close(). Nó xảy ra tự động anyway, finalizer sẽ chăm sóc nó. Ngay cả khi điều đó không xảy ra vì một số lý do (sự cố hoặc hỏng chương trình) sau đó Windows đảm bảo cổng được đóng lại.

+0

Cảm ơn Hans, điều đó rất hữu ích. Tôi đã thêm vào các lớp học đầy đủ trong trường hợp nó giúp bất cứ ai xác định vấn đề, nhưng tôi nghi ngờ tôi chỉ là lỗi của lỗi này. Tôi sẽ xem xét không gọi 'Close()' và xem cách tôi tiếp tục. – barry

+1

@Hans: Có cách nào tốt để đóng cổng nối tiếp trong trường hợp nó được cho là sẵn sàng để sử dụng bởi một chương trình khác hoặc xử lý khả năng bộ điều hợp nối tiếp USB bị ngắt kết nối bất ngờ? Cách tiếp cận mà tôi đã kết thúc bằng cách sử dụng là viết một chương trình có mục đích là mở một cổng nối tiếp và chuyển tiếp dữ liệu đến/từ chương trình chính. Nếu nó cần thiết để đóng cổng hoặc nếu nó chết, chương trình đó có thể chết mà không ảnh hưởng đến chương trình chính. Điều đó có vẻ hơi kỳ cục. – supercat

2

Tôi biết đây là câu hỏi khá cũ nhưng hiện tại. Tôi đã gặp sự cố này gần đây và sau khi tìm kiếm giải pháp, có vẻ như vấn đề này cuối cùng đã được khắc phục bằng Khuôn khổ .NET 4.7, theo ghi chú phát hành. https://github.com/Microsoft/dotnet/blob/master/releases/net47/dotnet47-changes.md

Cố định một vấn đề trong SerialPort nơi rút các thiết bị trong quá trình thực thể gây ra rò rỉ bộ nhớ trong lớp SerialStream.[288363]

Các vấn đề liên quan