Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DCS sequences split across multiple "packets" can be corrupted by conpty #17117

Closed
j4james opened this issue Apr 23, 2024 · 6 comments · Fixed by #17194
Closed

DCS sequences split across multiple "packets" can be corrupted by conpty #17117

j4james opened this issue Apr 23, 2024 · 6 comments · Fixed by #17194
Labels
Area-VT Virtual Terminal sequence support Issue-Bug It either shouldn't be doing this or needs an investigation. Needs-Tag-Fix Doesn't match tag requirements Product-Conpty For console issues specifically related to conpty

Comments

@j4james
Copy link
Collaborator

j4james commented Apr 23, 2024

Windows Terminal version

1.20.10822.0

Windows build number

10.0.19045.4291

Other Software

No response

Steps to reproduce

Run the following python script:

import sys
import time

sys.stdout.write('\033[37;40m\033[2J')
sys.stdout.write('\033P2$p0;2;50;0;0/')
sys.stdout.flush()
time.sleep(0.1)
sys.stdout.write('0;2;0;0;0\033\\')

Expected Behavior

It should clear the screen and briefly make the background color red. It works as expected in OpenConsole.

Actual Behavior

In Windows Terminal, the background is left permanently red, and you can see the second half of the DCS sequence output on the screen: 0;2;0;0;0.

Looking at the debug tap, it appears that conpty is inserting an SGR reset sequence in the middle of the DCS stream (where the script sleeps for a bit). This seems similar to issue #16079, but that test case is now working correctly on the main branch, while this is still broken.

I haven't had a chance to git bisect it, so I'm not sure if it's a regression, but I wouldn't be surprised if it was always broken in some way (although possibly not obviously so). There may also be an element of timing involved, but I can reproduce the problem consistently with the script above.

@j4james j4james added Issue-Bug It either shouldn't be doing this or needs an investigation. Needs-Triage It's a new issue that the core contributor team needs to triage at the next triage meeting labels Apr 23, 2024

This comment was marked as off-topic.

@lhecker

This comment was marked as off-topic.

@j4james
Copy link
Collaborator Author

j4james commented Apr 24, 2024

Once it does, entire rows are given the attribute at a time.

This is expected behavior. When the screen scrolls, the newly revealed line is meant to be filled with the active color attributes by default. You can control that behavior with DECECM (Erase Color Mode). For example, in pwsh you would need to do this first: "`e[?117h".

@carlos-zamora carlos-zamora added this to the Terminal v1.22 milestone Apr 25, 2024
@carlos-zamora carlos-zamora added Product-Conpty For console issues specifically related to conpty Area-VT Virtual Terminal sequence support labels Apr 25, 2024
@carlos-zamora carlos-zamora removed the Needs-Triage It's a new issue that the core contributor team needs to triage at the next triage meeting label Apr 25, 2024
@j4james
Copy link
Collaborator Author

j4james commented Apr 28, 2024

I figured out the problem here. When performing a DCS sequence passthrough, the first thing we do is call Renderer::TriggerFlush to make sure any pending VT engine paints are output before starting the actual DCS sequence. However, if there is already a pending paint waiting on the console lock in the render thread, that's still going to be executed when the lock is next free, even if there's nothing left for it to do.

So what's happening in this case is the initial part of the DCS gets passed through, then the console lock is released and the VT render engine proceeds with its paint. At this point there's nothing left for it paint, but that doesn't stop it from resetting the attributes in the "Prep Colors" step of _PaintFrameForEngine. That SGR sequence then ends up in the middle of the ongoing DCS sequence, resulting in an early termination.

As a quick hack fix, I made the Renderer::TriggerFlush method toggle the console lock after calling _PaintFrame. That gives the render thread a chance to grab the lock and flush its pending paint, but this feels like a very flaky solution. I think it would probably be better if the VT render engine could just avoid sending an SGR reset when there is no real need for it. But I don't know the code well enough to know if that's a workable solution.

@j4james
Copy link
Collaborator Author

j4james commented Apr 28, 2024

After further investigation into why the VT render engine is sending that SGR reset, I've discovered that that is technically a bug.

In theory, the VtEngine::StartPaint method already has a check to see whether there's something to do. See here:

// If there's nothing to do, quick return
auto somethingToDo = _invalidMap.any() ||
_scrollDelta != til::point{ 0, 0 } ||
_cursorMoved ||
_titleChanged;

If not, that method returns S_FALSE, which should indicate to the renderer that we can skip this frame entirely. However, the derived XtermEngine class ignores the S_FALSE return value, so that information is lost. See here:

[[nodiscard]] HRESULT XtermEngine::StartPaint() noexcept
{
RETURN_IF_FAILED(VtEngine::StartPaint());

The other problem is that the somethingToDo calculation always evaluates to true, because the _cursorMoved flag is always set to true. This is the result of the PR #15500 changes described in issue #17150 - we're now calling InvalidateCursor on every paint, which makes the VT engine think the cursor is always moving.

In theory that could be fixed by checking whether the cursor position in the InvalidateCursor call is different from the current position, if it weren't for the fact that the InvalidateCursor call isn't actually passed the current position - it's passed the previous position. But that should be resolved by my suggested fix for #17150.

In short, I think we could fix this bug with the following:

  1. Issue Issues with cursor invalidation in GDI #17150 would need to be fixed first.
  2. We would need to check for a change in position when setting the _cursorMoved flag in VtEngine::InvalidateCursor.
  3. The XtermEngine::StartPaint method would need to check for S_FALSE being returned from the parent class.

This way we wouldn't need to mess with the console lock, so it seems like a safer approach. There might be edge cases that I'm missing though.

@j4james
Copy link
Collaborator Author

j4james commented Apr 29, 2024

After trying this out, I found there's an extra step required. When the XtermEngine::StartPaint method returns S_FALSE, it also needs to reset the _noFlushOnEnd flag, otherwise the next frame may not flush when it is supposed to.

@lhecker lhecker removed their assignment Apr 29, 2024
@DHowett DHowett closed this as completed in 432dfcc May 6, 2024
@microsoft-github-policy-service microsoft-github-policy-service bot added the Needs-Tag-Fix Doesn't match tag requirements label May 6, 2024
DHowett pushed a commit that referenced this issue May 6, 2024
When the VT render engine starts a paint operation, it first checks to
see whether there is actually something to do, and if not it can end the
frame early. However, the result of that check was being ignored, which
could sometimes result in an unwanted `SGR` reset being written to the
conpty pipe.

This was particular concerning when passing through `DCS` sequences,
because an unexpected `SGR` in the middle of the `DCS` string would
cause it to abort early.

This PR addresses the problem by making sure the `VtEngine::StartPaint`
return value is appropriately handled in the `XtermEngine` class.

## Detailed Description of the Pull Request / Additional comments

To make this work, I also needed to correct the `_cursorMoved` flag,
because that is one of things that determines whether a paint is needed
or not, but it was being set in the `InvalidateCursor` method at the
start of ever frame, regardless of whether the cursor had actually
moved.

I also took this opportunity to get rid of the `_WillWriteSingleChar`
method and the `_quickReturn` flag, which have been mostly obsolete for
a long time now. The only place the flag was still used was to optimize
single char writes when line renditions are active. But that could more
easily be handled by testing the `_invalidMap` directly.

## Validation Steps Performed

I've confirmed that the test case in issue #17117 is no longer aborting
the `DCS` color table sequence early.

Closes #17117

(cherry picked from commit 432dfcc)
Service-Card-Id: 92501130
Service-Version: 1.21
github-merge-queue bot pushed a commit that referenced this issue May 10, 2024
When the VT render engine checks whether the cursor has moved in the
`InvalidateCursor` method, it does so by comparing the origin of the
given cursor region with the last text output coordinates. But these two
values are actually from different coordinate systems, and when on a
double-width line, the x text coordinate is half of the corresponding
screen coordinate. As a result, the movement detection is sometimes
incorrect.

This PR fixes the issue by adding another field to track the last cursor
origin in screen coordinates, so we have a meaningful value to compare
against.

## References and Relevant Issues

The previous cursor movement detection was added in PR #17194 to fix
issue #17117.

## Validation Steps Performed

I've confirmed that the test case from issue #17232 is now fixed, and
the test case from issue #17117 is still working as expected.

## PR Checklist
- [x] Closes #17232
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-VT Virtual Terminal sequence support Issue-Bug It either shouldn't be doing this or needs an investigation. Needs-Tag-Fix Doesn't match tag requirements Product-Conpty For console issues specifically related to conpty
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants