Skip to content

System.IO.Ports: check device capabilities for DTR/DSR in SerialStream Constructor/dispose (Windows)#122454

Closed
Benkol003 wants to merge 4 commits intodotnet:mainfrom
Benkol003:System.IO.Ports/check-device-capabilities
Closed

System.IO.Ports: check device capabilities for DTR/DSR in SerialStream Constructor/dispose (Windows)#122454
Benkol003 wants to merge 4 commits intodotnet:mainfrom
Benkol003:System.IO.Ports/check-device-capabilities

Conversation

@Benkol003
Copy link

See #121468
Fixes #27729

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Dec 11, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Dec 11, 2025
@Benkol003
Copy link
Author

@dotnet-policy-service agree

@Benkol003 Benkol003 force-pushed the System.IO.Ports/check-device-capabilities branch from b29505d to 8229338 Compare December 11, 2025 20:34
@Benkol003 Benkol003 changed the title System.IO.Ports: check device capabilities for DTR/RTS in SerialStream Constructor/dispose (Windows) System.IO.Ports: check device capabilities for DTR/DSR in SerialStream Constructor/dispose (Windows) Dec 17, 2025
@Benkol003 Benkol003 changed the title System.IO.Ports: check device capabilities for DTR/DSR in SerialStream Constructor/dispose (Windows) System.IO.Ports: check device capabilities for DTR/RTS in SerialStream Constructor/dispose (Windows) Dec 17, 2025
@vcsjones vcsjones added area-System.IO.Ports and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Dec 17, 2025
@Benkol003 Benkol003 changed the title System.IO.Ports: check device capabilities for DTR/RTS in SerialStream Constructor/dispose (Windows) System.IO.Ports: check device capabilities for DTR/DSR in SerialStream Constructor/dispose (Windows) Dec 18, 2025
Benkol003 and others added 2 commits December 27, 2025 22:24
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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@krwq krwq Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding explicit bool flag somewhere, i.e. _isXYZSupported

Copy link
Member

@krwq krwq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on finding good way to check for capability. I added some comments you might want to address

@krwq krwq added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jan 14, 2026
@krwq krwq self-assigned this Jan 14, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Comment on lines +642 to +650
// 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;
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment has a spelling error: "if" should be capitalized to "If" at the beginning of the sentence.

Suggested change
//if device supports DTR then clear
//If device supports DTR then clear

Copilot uses AI. Check for mistakes.
Comment on lines +637 to +651
//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;
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +724 to 752
//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();
}
}
}
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@krwq
Copy link
Member

krwq commented Jan 15, 2026

@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

@dotnet-policy-service
Copy link
Contributor

This pull request has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@dotnet-policy-service
Copy link
Contributor

This pull request will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the pull request, but please note that it will be locked if it remains inactive for another 30 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.IO.Ports community-contribution Indicates that the PR has been added by a community member needs-author-action An issue or pull request that requires more info or actions from the author. no-recent-activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SerialPort Open fails with 0x8007001f

5 participants