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

PresentationFramework.TextStore.GrantLock hangs application on Windows 11 #10032

Open
cbra-caa opened this issue Nov 4, 2024 · 4 comments
Open
Labels
Investigate Requires further investigation by the WPF team.

Comments

@cbra-caa
Copy link

cbra-caa commented Nov 4, 2024

Description

We have a AutoCompleter in our project which uses a RichTextBox internally. When the user writes text we search, and return an object which matches the entered text. In that case we delete the text and replace it with an InlineUIContainer which then displays the found item. The items are interactive and the user might use their contextmenu or copy them to another place.

Currently when users try to perform either of these actions the client hangs, and finally is killed of by the OS.

I have created a MVP which displays the problem which can be seen in the following gif:
Image

If you take a dump of the file, or pauses the Application with a debugger attached the stalling seems to originate from the following stacktrace where the 3 highlighted lines always are present:

PresentationFramework.dll!System.Windows.Documents.TextSchema.IsNonMergeableInline(System.Type elementType) Unknown PresentationFramework.dll!System.Windows.Documents.TextSchema.IsMergeableInline(System.Type elementType) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointerBase.GetBorderingElementCategory(System.Windows.Documents.ITextPointer position, System.Windows.Documents.LogicalDirection direction) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointerBase.IsAtNormalizedPosition(System.Windows.Documents.ITextPointer position, bool respectCaretUnitBoundaries) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointerBase.NormalizePosition(System.Windows.Documents.ITextPointer thisNavigator, System.Windows.Documents.LogicalDirection direction, bool respectCaretUnitBoundaries) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointer.MoveToInsertionPosition(System.Windows.Documents.LogicalDirection direction) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointer.System.Windows.Documents.ITextPointer.MoveToInsertionPosition(System.Windows.Documents.LogicalDirection direction) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointerBase.MoveToLineBoundary(System.Windows.Documents.ITextPointer thisPointer, System.Windows.Documents.ITextView textView, int count, bool respectNonMeargeableInlineStart) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointer.MoveToLineBoundary(int count) Unknown PresentationFramework.dll!System.Windows.Documents.TextPointer.System.Windows.Documents.ITextPointer.MoveToLineBoundary(int count) Unknown PresentationFramework.dll!System.Windows.Documents.TextStore.MS.Win32.UnsafeNativeMethods.ITextStoreACP.GetTextExt(int viewCookie, int startIndex, int endIndex, out MS.Win32.UnsafeNativeMethods.RECT rect, out bool clipped) Unknown [Native to Managed Transition] [Managed to Native Transition]
PresentationFramework.dll!System.Windows.Documents.TextStore.GrantLock() Unknown PresentationFramework.dll!System.Windows.Documents.TextStore.GrantLockWorker(MS.Win32.UnsafeNativeMethods.LockFlags flags) Unknown PresentationFramework.dll!System.Windows.Documents.TextStore.RequestLock(MS.Win32.UnsafeNativeMethods.LockFlags flags, out int hrSession) Unknown
[Native to Managed Transition] [Managed to Native Transition] WindowsBase.dll!System.Windows.Threading.Dispatcher.GetMessage(ref System.Windows.Interop.MSG msg, System.IntPtr hwnd, int minMessage, int maxMessage) Unknown WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame frame) Unknown WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame frame) Unknown PresentationFramework.dll!System.Windows.Application.RunDispatcher(object ignore) Unknown PresentationFramework.dll!System.Windows.Application.RunInternal(System.Windows.Window window) Unknown PresentationFramework.dll!System.Windows.Application.Run(System.Windows.Window window) Unknown PresentationFramework.dll!System.Windows.Application.Run() Unknown Win11TextPointerHang.exe!Win11TextPointerHang.App.Main() Unknown

Inspecting it in the Windows Event Log it seems to be a 4-step cycle:

Problem signature
Problem Event Name: AppHangB1
Application Name: Win11TextPointerHang.exe
Application Version: 1.0.0.0
Application Timestamp: 6720fc2b
Hang Signature: bec4
Hang Type: 134217728
OS Version: 10.0.22631.2.0.0.768.101
Locale ID: 1030
Additional Hang Signature 1: bec4eacce45c87d222cf0c1d657a150a
Additional Hang Signature 2: c247
Additional Hang Signature 3: c24739b36124a85c61b2550e90c2503b
Additional Hang Signature 4: bec4
Additional Hang Signature 5: bec4eacce45c87d222cf0c1d657a150a
Additional Hang Signature 6: c247
Additional Hang Signature 7: c24739b36124a85c61b2550e90c2503b

Reproduction Steps

Can be repoduced as follows.

  1. Build the below as a WPF Framework 4.8 App.
  2. Then start the application and write text in the textbox such that the next line in the textbox is activated f.ex. 'testing' will do the trick.
  3. Then press the button to convert the text to delete the old text and convert it to an InlineUIContainer.
  4. Then try to either select all the text in the textbox or spawn a contextmenu for the new item.
  5. Hang.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace Win11TextPointerHang
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            InitializeTextBox();
        }

        private void Button_Click(object sender, RoutedEventArgs e) 
            => ReplaceCurrentTextWithItem();

        private void ReplaceCurrentTextWithItem()
        {
            if (!GetCurrentText(out TextPointer pos, out string text))
                return;

            // This line seems to be the trigger point for the error.
            pos.DeleteTextInRun(-text.Length);

            TextBoxInlines.Add(BuildItem(text));
        }

        private bool GetCurrentText(out TextPointer pos, out string beforeText)
        {
            pos = ItemsTextBox.CaretPosition;
            if (pos == null)
            {
                beforeText = null;
                return false;
            }

            beforeText = pos.GetTextInRun(LogicalDirection.Backward) ?? "";
            return true;
        }

        #region Helpers

        private InlineCollection TextBoxInlines { get; set; }

        private void InitializeTextBox()
        {
            var paragraph = new Paragraph();
            ItemsTextBox.Document.Blocks.Add(paragraph);

            TextBoxInlines = paragraph.Inlines;
            TextBoxInlines.Add(BuildItem("A really long line that takes most of the space"));
        }

        private InlineUIContainer BuildItem(string text)
            => new InlineUIContainer(new Label
            {
                Content = text,
                Background = Brushes.Yellow,
                Margin = new Thickness(0, 0, 6, 0)
            });

        #endregion
    }
}
<Window x:Class="Win11TextPointerHang.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Win11TextPointerHang"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <RichTextBox x:Name="ItemsTextBox" Height="100" Width="300" Margin="0 0 12 12" />

        <Button x:Name="Button"
                Height="28"
                HorizontalAlignment="Center"
                Content="Convert text to item"
                Click="Button_Click"/>
    </StackPanel>
</Window>

Expected behavior

You can delete text in a textrun via the TextPointer.DeleteTextInRun.

Actual behavior

Deleting text with TextPointer.DeleteTextInRun hangs the applicaiton.

Regression?

Works in Windows 10 - seems isolated to Windows 11

Known Workarounds

No response

Impact

The bug impacts all users on Windows 11 which try to interact (selection or context menu) with an item which has just been created in a RichTextBox.

Configuration

.Net:
Framework 4.8

OS:
Windows 11 Pro for Workstations
23H2 - 22631.4391

**Architecture **
x64

Configuration
Observed across multiple different computers which all runs Windows 11. Windows 10 cannot reproduce the problem.
First observed around mid June this year, but we have few customers on Windows 11 so might have existed for longer.

Other information

OBS: There seems to be some limitations to the problem.

  • It cannot be the first item
  • You can still select/spawn contextmenu for the first item after the problem is in an active state.
  • The problem only arises if you interact with the new elements.
  • The problem only arises if the TextPointer.DeleteTextInRun is called, just creating InlineUIContainers cannot reproduce the problem.
  • The problem only arises if the new InlineUIContainer spawns on a new line in the RichTextBox
@lindexi
Copy link
Member

lindexi commented Nov 5, 2024

I can repro your issues. I debug the code and find the width of the result from GetLineBounds is zero.

And the navigator.MoveToLineBoundary(1) will do nothing, the MoveToLineBoundary always stood still. The position = textView.GetPositionAtNextLine(thisPointer, Double.NaN, count, out newSuggestedX, out count); move it to next line, but the position.MoveToInsertionPosition(position.LogicalDirection); bring it back.

    position = textView.GetPositionAtNextLine(thisPointer, Double.NaN, count, out newSuggestedX, out count);

     if (!position.IsAtInsertionPosition) 
     { 
         if (!respectNonMeargeableInlineStart ||  
             (!IsAtNonMergeableInlineStart(position) && !IsAtNonMergeableInlineEnd(position))) 
         { 
             position.MoveToInsertionPosition(position.LogicalDirection); 
         } 
     } 

internal static int MoveToLineBoundary(ITextPointer thisPointer, ITextView textView, int count, bool respectNonMeargeableInlineStart)
{
ITextPointer position;
double newSuggestedX;
Invariant.Assert(!thisPointer.IsFrozen, "Can't reposition a frozen pointer!");
Invariant.Assert(textView != null, "Null TextView!"); // Did you check ITextPointer.HasValidLayout?
position = textView.GetPositionAtNextLine(thisPointer, Double.NaN, count, out newSuggestedX, out count);
if (!position.IsAtInsertionPosition)
{
if (!respectNonMeargeableInlineStart ||
(!IsAtNonMergeableInlineStart(position) && !IsAtNonMergeableInlineEnd(position)))
{
position.MoveToInsertionPosition(position.LogicalDirection);
}
}
if (IsAtRowEnd(position))
{
// We will find outselves at a row end when we have incomplete
// markup like
//
// <TableCell></TableCell> <!-- No inner Run! -->
//
// In that case the end-of-row is the entire line.
thisPointer.MoveToPosition(position);
thisPointer.SetLogicalDirection(position.LogicalDirection);
}
else
{
TextSegment lineRange = textView.GetLineRange(position);
if (!lineRange.IsNull)
{
thisPointer.MoveToPosition(lineRange.Start);
thisPointer.SetLogicalDirection(lineRange.Start.LogicalDirection);
}
else if (count > 0)
{
// It is possible to get a non-zero return value from ITextView.GetPositionAtNextLine
// when moving into a BlockUIContainer. The container is the "next line" but does
// not contain any lines itself -- GetLineRange will return null.
thisPointer.MoveToPosition(position);
thisPointer.SetLogicalDirection(position.LogicalDirection);
}
}
return count;
}

But the arrange alogrithm of RichTextBox is too complex, that I can not find the main reason.

It seems that the MoveToLineBoundary should not return the count parameter directly, and I think the MoveToLineBoundary should return zero when the position is same as thisPointer. And the zero return code can break the loop by moved = (navigator.MoveToLineBoundary(1) != 0) ? true : false;.

lindexi added a commit to lindexi/lindexi_gd that referenced this issue Nov 5, 2024
@himgoyalmicro
Copy link
Contributor

Hey @cbra-caa is this issue reproducible in .NET Core as well?

@himgoyalmicro himgoyalmicro added the 📭 waiting-author-feedback To request more information from author. label Nov 6, 2024
@lindexi
Copy link
Member

lindexi commented Nov 6, 2024

@himgoyalmicro Yes, it can repro in .NET 8 and .NET 9

@cbra-caa
Copy link
Author

cbra-caa commented Nov 6, 2024

@himgoyalmicro I can reproduce it in .NET 8 as well

@dotnet-policy-service dotnet-policy-service bot removed the 📭 waiting-author-feedback To request more information from author. label Nov 6, 2024
@harshit7962 harshit7962 added the Investigate Requires further investigation by the WPF team. label Nov 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Investigate Requires further investigation by the WPF team.
Projects
None yet
Development

No branches or pull requests

4 participants