我真的很难在C#中使用SerialPort
,并发现自2010年以来,.NET中串行端口的API存在漏洞。我已将{{3}中的补丁组合在一起}和this blog post。
但是,当我尝试关闭我的串口时,我仍然在GC线程中收到一个未处理的异常,导致程序崩溃。有趣的是,堆栈跟踪的顶部恰好是SerialStream
Finalize()
方法,即使SerialStream
对象已经是GC.SuppressFinalize()
的答案代码我联系了。
这是例外:
System.ObjectDisposedException: Safe handle has been closed
at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success)
at System.StubHelpers.StubHelpers.SafeHandleAddRef(SafeHandle pHandle, Boolean& success)
at Microsoft.Win32.UnsafeNativeMethods.SetCommMask(SafeFileHandle hFile, Int32 dwEvtMask)
at System.IO.Ports.SerialStream.Dispose(Boolean disposing)
at System.IO.Ports.SerialStream.Finalize()
这里是串口调试的日志:
2017-20-07 14:29:22, Debug, Working around .NET SerialPort class Dispose bug
2017-20-07 14:29:22, Debug, Waiting for the SerialPort internal EventLoopRunner thread to finish...
2017-20-07 14:29:22, Debug, Wait completed. Now it is safe to continue disposal.
2017-20-07 14:29:22, Debug, Disposing internal serial stream
2017-20-07 14:29:22, Debug, Disposing serial port
这是我的代码,我发布的2个链接的略微修改版本:
public static class SerialPortFixerExt
{
public static void SafeOpen(this SerialPort port)
{
using(new SerialPortFixer(port.PortName))
{
}
port.Open();
}
public static void SafeClose(this SerialPort port, Logger logger)
{
try
{
Stream internalSerialStream = (Stream)port.GetType()
.GetField("internalSerialStream", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(port);
GC.SuppressFinalize(port);
GC.SuppressFinalize(internalSerialStream);
ShutdownEventLoopHandler(internalSerialStream, logger);
try
{
logger.Debug("serial-port-debug", "Disposing internal serial stream");
internalSerialStream.Close();
}
catch(Exception ex)
{
logger.Debug("serial-port-debug", String.Format("Exception in serial stream shutdown of port {0}: ", port.PortName), ex);
}
try
{
logger.Debug("serial-port-debug", "Disposing serial port");
port.Close();
}
catch(Exception ex)
{
logger.Debug("serial-port-debug", String.Format("Exception in port {0} shutdown: ", port.PortName), ex);
}
}
catch(Exception ex)
{
logger.Debug("serial-port-debug", String.Format("Exception in port {0} shutdown: ", port.PortName), ex);
}
}
static void ShutdownEventLoopHandler(Stream internalSerialStream, Logger logger)
{
try
{
logger.Debug("serial-port-debug", "Working around .NET SerialPort class Dispose bug");
FieldInfo eventRunnerField = internalSerialStream.GetType()
.GetField("eventRunner", BindingFlags.NonPublic | BindingFlags.Instance);
if(eventRunnerField == null)
{
logger.Warning("serial-port-debug",
"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)
{
logger.Warning("serial-port-debug",
"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
{
logger.Debug("serial-port-debug",
"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));
logger.Debug("serial-port-debug", "Wait completed. Now it is safe to continue disposal.");
}
}
}
catch(Exception ex)
{
logger.Warning("serial-port-debug",
"SerialPort workaround failure. Application may crash after "
+ "disposing SerialPort unless .NET 1.1 unhandled exception "
+ "policy is enabled from the application's config file: " +
ex);
}
}
}
public class SerialPortFixer : IDisposable
{
#region IDisposable Members
public void Dispose()
{
if(m_Handle != null)
{
m_Handle.Close();
m_Handle = null;
}
}
#endregion
#region Implementation
private const int DcbFlagAbortOnError = 14;
private const int CommStateRetries = 10;
private SafeFileHandle m_Handle;
internal SerialPortFixer(string portName)
{
const int dwFlagsAndAttributes = 0x40000000;
const int dwAccess = unchecked((int)0xC0000000);
if((portName == null) || !portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Invalid Serial Port", "portName");
}
SafeFileHandle hFile = CreateFile(@"\\.\" + portName, dwAccess, 0, IntPtr.Zero, 3, dwFlagsAndAttributes,
IntPtr.Zero);
if(hFile.IsInvalid)
{
WinIoError();
}
try
{
int fileType = GetFileType(hFile);
if((fileType != 2) && (fileType != 0))
{
throw new ArgumentException("Invalid Serial Port", "portName");
}
m_Handle = hFile;
InitializeDcb();
}
catch
{
hFile.Close();
m_Handle = null;
throw;
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int FormatMessage(int dwFlags, HandleRef lpSource, int dwMessageId, int dwLanguageId,
StringBuilder lpBuffer, int nSize, IntPtr arguments);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool GetCommState(SafeFileHandle hFile, ref Dcb lpDcb);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool SetCommState(SafeFileHandle hFile, ref Dcb lpDcb);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool ClearCommError(SafeFileHandle hFile, ref int lpErrors, ref Comstat lpStat);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
IntPtr securityAttrs, int dwCreationDisposition,
int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int GetFileType(SafeFileHandle hFile);
private void InitializeDcb()
{
Dcb dcb = new Dcb();
GetCommStateNative(ref dcb);
dcb.Flags &= ~(1u << DcbFlagAbortOnError);
SetCommStateNative(ref dcb);
}
private static string GetMessage(int errorCode)
{
StringBuilder lpBuffer = new StringBuilder(0x200);
if(
FormatMessage(0x3200, new HandleRef(null, IntPtr.Zero), errorCode, 0, lpBuffer, lpBuffer.Capacity,
IntPtr.Zero) != 0)
{
return lpBuffer.ToString();
}
return "Unknown Error";
}
private static int MakeHrFromErrorCode(int errorCode)
{
return (int)(0x80070000 | (uint)errorCode);
}
private static void WinIoError()
{
int errorCode = Marshal.GetLastWin32Error();
throw new IOException(GetMessage(errorCode), MakeHrFromErrorCode(errorCode));
}
private void GetCommStateNative(ref Dcb lpDcb)
{
int commErrors = 0;
Comstat comStat = new Comstat();
for(int i = 0; i < CommStateRetries; i++)
{
if(!ClearCommError(m_Handle, ref commErrors, ref comStat))
{
WinIoError();
}
if(GetCommState(m_Handle, ref lpDcb))
{
break;
}
if(i == CommStateRetries - 1)
{
WinIoError();
}
}
}
private void SetCommStateNative(ref Dcb lpDcb)
{
int commErrors = 0;
Comstat comStat = new Comstat();
for(int i = 0; i < CommStateRetries; i++)
{
if(!ClearCommError(m_Handle, ref commErrors, ref comStat))
{
WinIoError();
}
if(SetCommState(m_Handle, ref lpDcb))
{
break;
}
if(i == CommStateRetries - 1)
{
WinIoError();
}
}
}
#region Nested type: COMSTAT
[StructLayout(LayoutKind.Sequential)]
private struct Comstat
{
public readonly uint Flags;
public readonly uint cbInQue;
public readonly uint cbOutQue;
}
#endregion
#region Nested type: DCB
[StructLayout(LayoutKind.Sequential)]
private struct Dcb
{
public readonly uint DCBlength;
public readonly uint BaudRate;
public uint Flags;
public readonly ushort wReserved;
public readonly ushort XonLim;
public readonly ushort XoffLim;
public readonly byte ByteSize;
public readonly byte Parity;
public readonly byte StopBits;
public readonly byte XonChar;
public readonly byte XoffChar;
public readonly byte ErrorChar;
public readonly byte EofChar;
public readonly byte EvtChar;
public readonly ushort wReserved1;
}
#endregion
#endregion
}
使用该代码:
public void Connect(ConnectionParameters parameters)
{
if(m_params != null && m_params.Equals(parameters))
return; //already connected here
Close();
m_params = parameters;
try
{
m_comConnection = new SerialPort();
m_comConnection.PortName = m_params.Address;
m_comConnection.BaudRate = m_params.BaudRate;
m_comConnection.ReadTimeout = 6000;
m_comConnection.WriteTimeout = 6000;
m_comConnection.Parity = m_params.Parity;
m_comConnection.StopBits = m_params.StopBits;
m_comConnection.DataBits = m_params.DataBits;
m_comConnection.Handshake = m_params.Handshake;
m_comConnection.SafeOpen();
}
catch(Exception)
{
m_params = null;
m_comConnection = null;
throw;
}
}
public void Close()
{
if(m_params == null)
return;
m_comConnection.SafeClose(new Logger());
m_comConnection = null;
m_params = null;
}
那么我做错了什么?如果GC.SuppressFinalize()
未执行,我的日志看起来就不合适了。什么可以使Finalize()
仍然在GC.SuppressFinalize()
之后仍然执行?
我在端口的开启和关闭之间添加了我用SerialPort
的代码。
public byte[] ExchangeFrame(byte[] prefix, byte[] suffix, byte[] message, int length = -1)
{
if(m_params == null)
throw new NotConnectedException();
if(length == -1)
length = message.Length;
Write(prefix, 0, prefix.Length);
Write(message, 0, length);
Write(suffix, 0, suffix.Length);
byte[] response = new byte[length];
byte[] prefixBuffer = new byte[prefix.Length];
byte[] suffixBuffer = new byte[suffix.Length];
ReadTillFull(prefixBuffer);
while(!ByteArrayEquals(prefixBuffer, prefix))
{
for(int i = 0; i < prefixBuffer.Length - 1; i++)
prefixBuffer[i] = prefixBuffer[i + 1]; //shift them all back
if(Read(prefixBuffer, prefixBuffer.Length - 1, 1) == 0) //read one more
throw new NotConnectedException("Received no data when reading prefix.");
}
ReadTillFull(response);
ReadTillFull(suffixBuffer);
if(!ByteArrayEquals(suffixBuffer, suffix))
throw new InvalidDataException("Frame received matches prefix but does not match suffix. Response: " + BitConverter.ToString(prefixBuffer) + BitConverter.ToString(response) + BitConverter.ToString(suffixBuffer));
return response;
}
private void ReadTillFull(byte[] buffer, int start = 0, int length = -1)
{
if(length == -1)
length = buffer.Length;
int remaining = length;
while(remaining > 0)
{
int read = Read(buffer, start + length - remaining, remaining);
if(read == 0)
throw new NotConnectedException("Received no data when reading suffix.");
remaining -= read;
}
}
//this method looks dumb but actually, the real code has a switch statement to check if should connect by TCP or Serial
private int Read(byte[] buffer, int start, int length)
{
return m_comConnection.Read(buffer, start, length);
}
private void Write(byte[] buffer, int start, int length)
{
m_comConnection.Write(buffer, start, length);
}
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int memcmp(byte[] b1, byte[] b2, long count);
static bool ByteArrayEquals(byte[] b1, byte[] b2)
{
// Validate buffers are the same length.
// This also ensures that the count does not exceed the length of either buffer.
return b1.Length == b2.Length && memcmp(b1, b2, b1.Length) == 0;
}
目前,我只是通过打开,写入,阅读和关闭端口来寻找健康的通信。我将不得不处理端口物理拔出的情况。(最终)
请注意,我连续多次使用ExchangeFrame
方法(在循环中)。经过更多测试后,我发现一旦没有产生任何错误就使用它。 (发送一个帧)我想我的代码存在问题,但我不确定我在做什么,多次执行会导致SerialPort
中断。
另一方面,我还有另一个问题,即我在不时阅读的框架中收到大量的0。 (没有注意到一种模式)我不知道为什么,但我知道这个代码出现了一个新的错误。不是问题的一部分,但可能是相关的。
问题原因是我自己用Thread.Interrupt()
打断了SerialPort。我正确地处理了ThreadInterruptedException
,但它在内部破坏了SerialPort。以下是导致此问题的代码:
public void Stop()
{
m_run = false;
if(m_acquisitor != null)
{
m_acquisitor.Interrupt();
m_acquisitor.Join();
}
m_acquisitor = null;
}
private void Run()
{
while(m_run)
{
long time = DateTime.Now.Ticks, done = -1;
double[] data;
try
{
lock(Connection)
{
if(!m_run)
break; //if closed while waiting for Connection
Connection.Connect(Device.ConnectionParams);
data = Acquire();
}
done = DateTime.Now.Ticks;
Ping = (done - time) / TimeSpan.TicksPerMillisecond;
Samples++;
if(SampleReceived != null)
SampleReceived(this, data);
}
catch(ThreadInterruptedException)
{
continue; //checks if m_run is true, if not quits
}
try
{
if(done == -1)
Thread.Sleep(RETRY_DELAY); //retry delay
else
Thread.Sleep((int)Math.Max(SamplingRate * 1000 - (done - time) / TimeSpan.TicksPerMillisecond, 0));
}
catch(ThreadInterruptedException) {} //continue
}
lock(Connection)
Connection.Close(); //serialport's wrapper
}
非常感谢that answer帮助我在聊天中解决问题。