System.IO.Ports: check device capabilities for DTR/DSR in SerialStream Constructor/dispose (Windows)#122454
Conversation
|
@dotnet-policy-service agree |
b29505d to
8229338
Compare
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: kasperk81 <[email protected]>
Co-authored-by: kasperk81 <[email protected]>
| @@ -634,17 +634,21 @@ internal SerialStream(string portName, int baudRate, Parity parity, int dataBits | |||
| // set constant properties of the DCB | |||
| InitializeDCB(baudRate, parity, dataBits, stopBits, discardNull); | |||
|
|
|||
| DtrEnable = dtrEnable; | |||
| //if device doesnt support DTR and DTR is disabled, then dont try to set DTR | |||
| if (DtrEnable || ((_commProp.dwProvCapabilities & Interop.Kernel32.COMMPROP.PCF_DTRDSR) != 0)) | |||
There was a problem hiding this comment.
I'm a bit confused on checking DtrEnable property in the if statement. Should this be local dtrEnable? I'm seeing you're assigning it two lines later but wouldn't this DtrEnable always evaluate to false given this is inside constructor and it's not assigned earlier?
Also I think it would be more appropriate to throw if we cannot do what user requested (since name suggests user wants it enabled it will be surprising if it's not in the end without explanation) rather than ignoring (we should match behavior with other properties and other OS implementations, even if it means less intuitive behavior).
I'd recommend doing something along the lines of (do it as early as appropriate):
if (dtrEnable && (_commProp.dwProvCapabilities & Interop.Kernel32.COMMPROP.PCF_DTRDSR) == 0))
{
throw new WhateverApplicableException(); // probably invalid operation but be consistent
}I'm also not sure why RTS setting is related to DTR being enabled - this should probably also check for feature presence and throw if not available.
I'd recommend to also try adding test case but it currently requires either two serial ports cross connected (null model) or single serial port with RX and TX connected (loopback). I'd appreciate if you could run it as it's currently manual. I think tests might be a bit aggressive how they detect it so make sure to not run it on machine which has anything fragile connected through serial port (i.e. production machine). Note that given complex nature of tests here it's not strict requirement but it will be more than welcome.
There was a problem hiding this comment.
For RTS I think you can use this flag:
PCF_RTSCTS 0x0002
| if (disposing) | ||
| throw Win32Marshal.GetExceptionForLastWin32Error(); | ||
| // access denied can happen if USB is yanked out. If that happens, we | ||
| // want to at least allow finalize to succeed and clean up everything |
There was a problem hiding this comment.
I'm missing the part where we do the cleanup referred in the comment
| @@ -717,29 +721,33 @@ protected override void Dispose(bool disposing) | |||
|
|
|||
| // turn off all events and signal WaitCommEvent | |||
| Interop.Kernel32.SetCommMask(_handle, 0); | |||
| if (!Interop.Kernel32.EscapeCommFunction(_handle, Interop.Kernel32.CommFunctions.CLRDTR)) | |||
| //if device supports DTR then clear | |||
| if ((_commProp.dwProvCapabilities & Interop.Kernel32.COMMPROP.PCF_DTRDSR) != 0) | |||
There was a problem hiding this comment.
consider adding explicit bool flag somewhere, i.e. _isXYZSupported
krwq
left a comment
There was a problem hiding this comment.
Nice work on finding good way to check for capability. I added some comments you might want to address
| // query and cache the initial RtsEnable value | ||
| // so that set_RtsEnable can do the (value != rtsEnable) optimization | ||
| _rtsEnable = (GetDcbFlag(Interop.Kernel32.DCBFlags.FRTSCONTROL) == Interop.Kernel32.DCBRTSFlowControl.RTS_CONTROL_ENABLE); | ||
|
|
||
| // now set this.RtsEnable to the specified value. | ||
| // Handshake takes precedence, this will be a nop if | ||
| // handshake is either RequestToSend or RequestToSendXOnXOff | ||
| if ((handshake != Handshake.RequestToSend && handshake != Handshake.RequestToSendXOnXOff)) | ||
| RtsEnable = rtsEnable; | ||
| // now set this.RtsEnable to the specified value. | ||
| // Handshake takes precedence, this will be a nop if | ||
| // handshake is either RequestToSend or RequestToSendXOnXOff | ||
| if ((handshake != Handshake.RequestToSend && handshake != Handshake.RequestToSendXOnXOff)) | ||
| RtsEnable = rtsEnable; |
There was a problem hiding this comment.
The RTS handling code (lines 642-650) should not be inside the DTR capability check. RTS (Request To Send) and DTR (Data Terminal Ready) are independent control signals. A device might support RTS but not DTR, or vice versa. Placing RTS setup inside the DTR capability check means RTS won't be configured if the device doesn't support DTR, which is incorrect behavior.
There was a problem hiding this comment.
I already also mentioned that in the comment above #122454 (comment)
| // turn off all events and signal WaitCommEvent | ||
| Interop.Kernel32.SetCommMask(_handle, 0); | ||
| if (!Interop.Kernel32.EscapeCommFunction(_handle, Interop.Kernel32.CommFunctions.CLRDTR)) | ||
| //if device supports DTR then clear |
There was a problem hiding this comment.
The comment has a spelling error: "if" should be capitalized to "If" at the beginning of the sentence.
| //if device supports DTR then clear | |
| //If device supports DTR then clear |
| //if device doesnt support DTR and DTR is disabled, then dont try to set DTR | ||
| if (DtrEnable || ((_commProp.dwProvCapabilities & Interop.Kernel32.COMMPROP.PCF_DTRDSR) != 0)) | ||
| { | ||
| DtrEnable = dtrEnable; | ||
|
|
||
| // query and cache the initial RtsEnable value | ||
| // so that set_RtsEnable can do the (value != rtsEnable) optimization | ||
| _rtsEnable = (GetDcbFlag(Interop.Kernel32.DCBFlags.FRTSCONTROL) == Interop.Kernel32.DCBRTSFlowControl.RTS_CONTROL_ENABLE); | ||
| // query and cache the initial RtsEnable value | ||
| // so that set_RtsEnable can do the (value != rtsEnable) optimization | ||
| _rtsEnable = (GetDcbFlag(Interop.Kernel32.DCBFlags.FRTSCONTROL) == Interop.Kernel32.DCBRTSFlowControl.RTS_CONTROL_ENABLE); | ||
|
|
||
| // now set this.RtsEnable to the specified value. | ||
| // Handshake takes precedence, this will be a nop if | ||
| // handshake is either RequestToSend or RequestToSendXOnXOff | ||
| if ((handshake != Handshake.RequestToSend && handshake != Handshake.RequestToSendXOnXOff)) | ||
| RtsEnable = rtsEnable; | ||
| // now set this.RtsEnable to the specified value. | ||
| // Handshake takes precedence, this will be a nop if | ||
| // handshake is either RequestToSend or RequestToSendXOnXOff | ||
| if ((handshake != Handshake.RequestToSend && handshake != Handshake.RequestToSendXOnXOff)) | ||
| RtsEnable = rtsEnable; | ||
| } |
There was a problem hiding this comment.
This change introduces new behavior to check device capabilities for DTR/DSR support, but there are no corresponding tests to verify this logic works correctly. Tests should be added to cover scenarios such as: devices that don't support DTR with dtrEnable=false (should skip setting DTR), devices that support DTR with both dtrEnable values, and ensuring RTS is still set independently of DTR capability.
| //if device supports DTR then clear | ||
| if ((_commProp.dwProvCapabilities & Interop.Kernel32.COMMPROP.PCF_DTRDSR) != 0) | ||
| { | ||
| int hr = Marshal.GetLastPInvokeError(); | ||
|
|
||
| // access denied can happen if USB is yanked out. If that happens, we | ||
| // want to at least allow finalize to succeed and clean up everything | ||
| // we can. To achieve this, we need to avoid further attempts to access | ||
| // the SerialPort. A customer also reported seeing ERROR_BAD_COMMAND here. | ||
| // Do not throw an exception on the finalizer thread - that's just rude, | ||
| // since apps can't catch it and we may tear down the app. | ||
| const int ERROR_DEVICE_REMOVED = 1617; | ||
| if ((hr == Interop.Errors.ERROR_ACCESS_DENIED || hr == Interop.Errors.ERROR_BAD_COMMAND || hr == ERROR_DEVICE_REMOVED) && !disposing) | ||
| { | ||
| skipSPAccess = true; | ||
| } | ||
| else | ||
| if (!Interop.Kernel32.EscapeCommFunction(_handle, Interop.Kernel32.CommFunctions.CLRDTR)) | ||
| { | ||
| // should not happen | ||
| Debug.Fail($"Unexpected error code from EscapeCommFunction in SerialPort.Dispose(bool) Error code: 0x{(uint)hr:x}"); | ||
| int hr = Marshal.GetLastPInvokeError(); | ||
|
|
||
| // Do not throw an exception from the finalizer here. | ||
| if (disposing) | ||
| throw Win32Marshal.GetExceptionForLastWin32Error(); | ||
| // access denied can happen if USB is yanked out. If that happens, we | ||
| // want to at least allow finalize to succeed and clean up everything | ||
| // we can. To achieve this, we need to avoid further attempts to access | ||
| // the SerialPort. A customer also reported seeing ERROR_BAD_COMMAND here. | ||
| // Do not throw an exception on the finalizer thread - that's just rude, | ||
| // since apps can't catch it and we may tear down the app. | ||
| const int ERROR_DEVICE_REMOVED = 1617; | ||
| if ((hr == Interop.Errors.ERROR_ACCESS_DENIED || hr == Interop.Errors.ERROR_BAD_COMMAND || hr == ERROR_DEVICE_REMOVED) && !disposing) | ||
| { | ||
| skipSPAccess = true; | ||
| } | ||
| else | ||
| { | ||
| // should not happen | ||
| Debug.Fail($"Unexpected error code from EscapeCommFunction in SerialPort.Dispose(bool) Error code: 0x{(uint)hr:x}"); | ||
|
|
||
| // Do not throw an exception from the finalizer here. | ||
| if (disposing) | ||
| throw Win32Marshal.GetExceptionForLastWin32Error(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This change introduces new behavior to check device capabilities for DTR/DSR support in Dispose, but there are no corresponding tests to verify this logic works correctly. Tests should be added to verify that devices without DTR support don't attempt to clear DTR during disposal, and that the existing error handling still works correctly.
|
@Benkol003 I think I already captured essence of copilot's feedback in my comments - feel free to ignore them. The tests is a hard subject because we don't have devices connected in CI - unless you find some smart way to emulate those but still tests wouldn't know which device have which capability so you'd essentially test feature with itself which makes it have less value - therefore manual test here is essential to bootstrap or to any changes related to feature presence checks |
|
This pull request has been automatically marked |
|
This pull request will now be closed since it had been marked |
See #121468
Fixes #27729