Cannot use stream-based I/O to ConPTY/Console and receive WINDOWS_BUFFER_SIZE_EVENT simultaneously #595

Open
opened 2026-01-30 21:56:26 +00:00 by claunia · 0 comments
Owner

Originally created by @oising on GitHub (Mar 25, 2019).

Microsoft Windows [Version 10.0.18362.1]

To monitor console buffer resize events (and viewport resize events in more recent builds), ReadConsoleInput must be used to look for INPUT_BUFFER events with an event type of 4 ( WINDOW_BUFFER_SIZE_EVENT ).

My problem is that when I open a pseudoconsole session for a new cmd.exe process and I want to use bidirectional VT codes (stream based) for stdin/stdout for maximum cross platform portability. That is to say, I don't want to use the windows console APIs because they are windows specific. This seems to make it very difficult - without gag-inducing and complex task synchronization - to watch for resize events at the same time. When stdin/stdout are flushed, any INPUT_BUFFER records in ReadConsoleInput are too, including WINDOWS_BUFFER_SIZE_EVENT records.

VT allows capturing mouse movement - or at least it should in the future since ConPTY doesn't seem to forward mouse coords/actions yet, but there is no equivalent VT sequence for resizes of the viewport. Here's some demo/repro code:

My specific NuGet dependencies for the repro are:

  • Vanara.PInvoke.Kernel32 / 2.3.4
  • Pipelines.Sockets.Unofficial / 2.0.7
using System;
using System.IO;
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

using Pipelines.Sockets.Unofficial;
using Vanara.PInvoke;

namespace resizetest
{
    internal class Program
    {
        private static async Task Main(string[] args)
        {
            // run for 30 seconds
            const int timeout = 30000;

            var source = new CancellationTokenSource(timeout); // 30 seconds
            var token = source.Token;
            var handle = Kernel32.GetStdHandle(Kernel32.StdHandleType.STD_INPUT_HANDLE);
            
            Kernel32.CONSOLE_MODE mode = default;

            if (!Kernel32.GetConsoleMode(handle, ref mode))
            {
                throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error());
            }
            
            mode |= Kernel32.CONSOLE_MODE.ENABLE_WINDOW_INPUT;
            mode |= Kernel32.CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_INPUT;
            mode &= ~Kernel32.CONSOLE_MODE.ENABLE_MOUSE_INPUT;

            if (!Kernel32.SetConsoleMode(handle, mode))
            {
                throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error());
            }

            Task drainStdin = Task.Run(async () =>
            {
                // UNCOMMENT THESE LINES TO ENABLE CAPTURE OF RESIZE EVENTS
                //await Task.Delay(timeout, token);
                //return;

                Stream stdin = Console.OpenStandardInput();
                PipeReader reader = StreamConnection.GetReader(stdin);

                while (!token.IsCancellationRequested)
                {
                    var result = await reader.ReadAsync(token);
                    if (result.IsCanceled || result.IsCompleted)
                    {
                        break;
                    }

                    // do stuff...

                    Console.WriteLine("stdin: read {0} byte(s)", result.Buffer.Length);
                }
            }, token);

            Task readInputBuffer = Task.Run(async () =>
            {
                while (!token.IsCancellationRequested)
                {
                    await Task.Delay(10, token);

                    if (!Kernel32.GetNumberOfConsoleInputEvents(handle, out var count))
                    {
                        throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error());
                    }

                    if (count > 0)
                    {
                        var buffer = new Kernel32.INPUT_RECORD[count];

                        if (!Kernel32.ReadConsoleInput(handle, buffer, count, out var total)) {
                            throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error());
                        }

                        if (total > 0)
                        {
                            // TODO: de-bounce and only send last resize? 
                            for (int index = 0; index < total; index++)
                            {
                                var record = buffer[index];

                                Console.WriteLine("type: {0}", record.EventType);

                                if (record.EventType == Kernel32.EVENT_TYPE.WINDOW_BUFFER_SIZE_EVENT) // resize
                                {
                                    var size = record.Event.WindowBufferSizeEvent.dwSize;
                                    Console.WriteLine("buffer: resize to {{{0}, {1}}}", size.X, size.Y);
                                }
                            }
                        }
                    }
                }

            }, token);

            Console.WriteLine("Running");

            try
            {
                await Task.WhenAll(readInputBuffer, drainStdin);
            }
            catch (OperationCanceledException)
            {
                // timeout
            }

            Console.WriteLine("press any key...");
            Console.ReadKey(true);

        }
    }
}

TL;DR -- it's very hard, if not impossible, to watch for viewport resizing and also use a pure VT implementation (no console API).

Solution? I'd suggest draining the input buffer if streaming but leaving resize, menu, mouse (if enabled) events etc. This may be a memory leak (but probably fixed max, circular buffer etc) if the default behaviour changed, so I guess it should be opt-in.

Originally created by @oising on GitHub (Mar 25, 2019). Microsoft Windows [Version 10.0.18362.1] To monitor console buffer resize events (and viewport resize events in more recent builds), `ReadConsoleInput` must be used to look for `INPUT_BUFFER` events with an event type of 4 ( `WINDOW_BUFFER_SIZE_EVENT` ). My problem is that when I open a pseudoconsole session for a new cmd.exe process and I want to use bidirectional VT codes (stream based) for stdin/stdout for maximum cross platform portability. That is to say, I don't want to use the windows console APIs because they are windows specific. This seems to make it very difficult - without gag-inducing and complex task synchronization - to watch for resize events at the same time. When stdin/stdout are flushed, any `INPUT_BUFFER` records in `ReadConsoleInput` are too, including `WINDOWS_BUFFER_SIZE_EVENT` records. VT allows capturing mouse movement - or at least it should in the future since ConPTY doesn't seem to forward mouse coords/actions yet, but there is no equivalent VT sequence for resizes of the viewport. Here's some demo/repro code: My specific NuGet dependencies for the repro are: - Vanara.PInvoke.Kernel32 / 2.3.4 - Pipelines.Sockets.Unofficial / 2.0.7 ``` using System; using System.IO; using System.IO.Pipelines; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; using Vanara.PInvoke; namespace resizetest { internal class Program { private static async Task Main(string[] args) { // run for 30 seconds const int timeout = 30000; var source = new CancellationTokenSource(timeout); // 30 seconds var token = source.Token; var handle = Kernel32.GetStdHandle(Kernel32.StdHandleType.STD_INPUT_HANDLE); Kernel32.CONSOLE_MODE mode = default; if (!Kernel32.GetConsoleMode(handle, ref mode)) { throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error()); } mode |= Kernel32.CONSOLE_MODE.ENABLE_WINDOW_INPUT; mode |= Kernel32.CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_INPUT; mode &= ~Kernel32.CONSOLE_MODE.ENABLE_MOUSE_INPUT; if (!Kernel32.SetConsoleMode(handle, mode)) { throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error()); } Task drainStdin = Task.Run(async () => { // UNCOMMENT THESE LINES TO ENABLE CAPTURE OF RESIZE EVENTS //await Task.Delay(timeout, token); //return; Stream stdin = Console.OpenStandardInput(); PipeReader reader = StreamConnection.GetReader(stdin); while (!token.IsCancellationRequested) { var result = await reader.ReadAsync(token); if (result.IsCanceled || result.IsCompleted) { break; } // do stuff... Console.WriteLine("stdin: read {0} byte(s)", result.Buffer.Length); } }, token); Task readInputBuffer = Task.Run(async () => { while (!token.IsCancellationRequested) { await Task.Delay(10, token); if (!Kernel32.GetNumberOfConsoleInputEvents(handle, out var count)) { throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error()); } if (count > 0) { var buffer = new Kernel32.INPUT_RECORD[count]; if (!Kernel32.ReadConsoleInput(handle, buffer, count, out var total)) { throw Marshal.GetExceptionForHR(Marshal.GetLastWin32Error()); } if (total > 0) { // TODO: de-bounce and only send last resize? for (int index = 0; index < total; index++) { var record = buffer[index]; Console.WriteLine("type: {0}", record.EventType); if (record.EventType == Kernel32.EVENT_TYPE.WINDOW_BUFFER_SIZE_EVENT) // resize { var size = record.Event.WindowBufferSizeEvent.dwSize; Console.WriteLine("buffer: resize to {{{0}, {1}}}", size.X, size.Y); } } } } } }, token); Console.WriteLine("Running"); try { await Task.WhenAll(readInputBuffer, drainStdin); } catch (OperationCanceledException) { // timeout } Console.WriteLine("press any key..."); Console.ReadKey(true); } } } ``` TL;DR -- it's very hard, if not impossible, to watch for viewport resizing and also use a pure VT implementation (no console API). Solution? I'd suggest draining the input buffer if streaming but leaving resize, menu, mouse (if enabled) events etc. This may be a memory leak (but probably fixed max, circular buffer etc) if the default behaviour changed, so I guess it should be opt-in.
claunia added the Product-ConhostIssue-QuestionResolution-AnsweredArea-Server labels 2026-01-30 21:56:26 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#595