Ctrl+C is sometimes ignored on Windows Terminal

Naoto Aoki naoto.november@gmail.com
Mon Nov 1 08:33:02 GMT 2021


Hi,

When I'm using some programs such as bash and python from cygwin under
Windows Terminal, Ctrl+C is sometimes ignored.
https://github.com/microsoft/terminal

Normally holding 'Ctrl' and pressing 'C' will make new line.
But, sometimes it does not and unholding 'Ctrl' makes new line under
Windows Terminal.
bash from msys2 does also reproduce this issue.

I dug into this issue and found that this is related to
readline and Windows 10's pseudo console (ConPTY).
I made simple programs to reproduce this issue.

 - EchoCon.cpp
   - modification of ConPty sample code provided by Microsoft.
     This program execute bash on pseudo console.
     to be compiled with MSVC.
 - getkey.cpp
   - simple program to check Ctrl+C is passed to Cygwin program.
     to be compiled with Cygwin gcc.
 - rltest.cpp
   - simple program to check SIGINT handling.
     This program reproduces the issue.
     If you replace readline("> ") with gets(buf),
     then the issue does not happen.
     to be compiled with Cygwin gcc.

- The machine and OS that it is running on
  - OS: Windows 10 Pro 19043.1288
  - Windows Terminal: 1.11.2921.0

Regards,
Naoto Aoki
-------------- next part --------------
// original: https://blog.goo.ne.jp/lm324/e/16629a8aadaa0de77fc05611390cf15b
// modified by aont 2021/10/15

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>

int getkey(void)
{
    struct termios oldt, newt;
    int ch0,ch1,ch2,ch3,ch4;
    int ret;

    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_iflag = ~( BRKINT | ISTRIP | IXON  );
    newt.c_lflag = ~( ICANON | IEXTEN | ECHO | ECHOE | ECHOK | ECHONL );
    newt.c_cc[VTIME]    = 0;
    newt.c_cc[VMIN]     = 1;
    newt.c_cc[VINTR]    = 1;
    if(tcsetattr(STDIN_FILENO, TCSANOW, &newt)==-1) {
        fprintf(stderr,"error tcsetattr\n");
        exit(EXIT_FAILURE);
    }
    
    ch0 = getchar();
    if(ch0==0x1B) {
        ch1 = getchar();
        ch2 = getchar();
        if(ch2==0x32) {
            ch3 = getchar();
            if(ch3==0x7e) {
                ret = (ch0<<24) | (ch1<<16) | (ch2<<8) | ch3;
            } else {
                ch4 = getchar();
                ret = (ch1<<24) | (ch2<<16) | (ch3<<8) | ch4;
            }
        } else if(ch2==0x31) {
            ch3 = getchar();
            ch4 = getchar();
            ret = (ch1<<24) | (ch2<<16) | (ch3<<8) | ch4;
        } else if((ch2==0x33)||(ch2==0x35)||(ch2==0x36)) {
            ch3 = getchar();
            ret = (ch0<<24) | (ch1<<16) | (ch2<<8) | ch3;
        } else {
            ret = (ch0<<16) | (ch1<<8) | ch2;
        }
    } else if(ch0 != EOF) {
        ret = ch0;
    } else {
        ret = 0;
    }

    if(tcsetattr(STDIN_FILENO, TCSANOW, &oldt)==-1) {
        fprintf(stderr,"error tcsetattr\n");
        exit(EXIT_FAILURE);
    }

    return ret;
}

int main(int argc,char *argv[])
{
    int key;

    while (1) {
        key = getkey();
        
        if (key!=0) {
            printf("key code 0x%x\n",key);
        }else{
            ;
            usleep(1000);
        }
        if(key==0x71 /* q */ ) {
            break;
        }

    }

    return 0;
}

-------------- next part --------------
#include <setjmp.h>
#include <stdio.h>
#include <signal.h>
#include <readline/readline.h>

sigjmp_buf ctrlc_buf;

void handle_signals(int signo) {
  if (signo == SIGINT) {
    siglongjmp(ctrlc_buf, 1);
  }
}

int main(int argc, char **argv)
{
  char * input;

  if (signal(SIGINT, handle_signals) == SIG_ERR) {
    fprintf(stderr, "installing signal handler failed\n");
  }

    char buf[128];
  while (1) 
  {
    while ( sigsetjmp( ctrlc_buf, 1 ) != 0 ) {
      fprintf(stderr, "Ctrl+C\n");
    }

    input = readline("> ");
    if (!input)
      break;

  }  
  return 0;
}
-------------- next part --------------
// EchoCon.cpp : Entry point for the EchoCon Pseudo-Console sample application.
// Copyright © 2018, Microsoft
// Modified by aont 2021/10/15

// #include "stdafx.h"
#include <Windows.h>
#include <process.h>
#include <cstdio>

// Forward declarations
HRESULT CreatePseudoConsoleAndPipes(HPCON*, HANDLE*, HANDLE*);
HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX*, HPCON);
void __cdecl PipeListener(LPVOID);

int main()
{
    wchar_t szCommand[]{ L"C:\\cygwin64\\bin\\bash.exe --login -i" };
    HRESULT hr{ E_UNEXPECTED };
    HANDLE hConsole = { GetStdHandle(STD_OUTPUT_HANDLE) };

    HANDLE hin = GetStdHandle(STD_INPUT_HANDLE);

    // Enable Console VT Processing
    DWORD consoleMode{};

    GetConsoleMode(hin, &consoleMode);
    hr = SetConsoleMode(hin, consoleMode ^ ENABLE_PROCESSED_INPUT ^ ENABLE_LINE_INPUT)
        ? S_OK
        : GetLastError();

    GetConsoleMode(hConsole, &consoleMode);
    hr = SetConsoleMode(hConsole, consoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
        ? S_OK
        : GetLastError();
    if (S_OK == hr)
    {
        HPCON hPC{ INVALID_HANDLE_VALUE };

        //  Create the Pseudo Console and pipes to it
        HANDLE hPipeIn{ INVALID_HANDLE_VALUE };
        HANDLE hPipeOut{ INVALID_HANDLE_VALUE };
        hr = CreatePseudoConsoleAndPipes(&hPC, &hPipeIn, &hPipeOut);
        if (S_OK == hr)
        {
            // Create & start thread to listen to the incoming pipe
            // Note: Using CRT-safe _beginthread() rather than CreateThread()
            HANDLE hPipeListenerThread{ reinterpret_cast<HANDLE>(_beginthread(PipeListener, 0, hPipeIn)) };

            // Initialize the necessary startup info struct        
            STARTUPINFOEX startupInfo{};
            if (S_OK == InitializeStartupInfoAttachedToPseudoConsole(&startupInfo, hPC))
            {
                // Launch ping to emit some text back via the pipe
                PROCESS_INFORMATION piClient{};
                hr = CreateProcess(
                    NULL,                           // No module name - use Command Line
                    szCommand,                      // Command Line
                    NULL,                           // Process handle not inheritable
                    NULL,                           // Thread handle not inheritable
                    FALSE,                          // Inherit handles
                    EXTENDED_STARTUPINFO_PRESENT,   // Creation flags
                    NULL,                           // Use parent's environment block
                    NULL,                           // Use parent's starting directory 
                    &startupInfo.StartupInfo,       // Pointer to STARTUPINFO
                    &piClient)                      // Pointer to PROCESS_INFORMATION
                    ? S_OK
                    : GetLastError();

                char input_key;
                DWORD num_events;
                while (true) {
                    bool ret;
                    ret = ReadFile(hin, &input_key, 1, &num_events, NULL);
                    if (ret) {
                        fprintf(stderr, "input: 0x%x\n", input_key);
                        WriteFile(hPipeOut, &input_key, 1, &num_events, NULL);
                    }
                    
                };


                if (S_OK == hr)
                {
                    // Wait up to 10s for ping process to complete
                    WaitForSingleObject(piClient.hThread, 10 * 1000);

                    // Allow listening thread to catch-up with final output!
                    Sleep(500);
                }

                // --- CLOSEDOWN ---

                // Now safe to clean-up client app's process-info & thread
                CloseHandle(piClient.hThread);
                CloseHandle(piClient.hProcess);

                // Cleanup attribute list
                DeleteProcThreadAttributeList(startupInfo.lpAttributeList);
                free(startupInfo.lpAttributeList);
            }

            // Close ConPTY - this will terminate client process if running
            ClosePseudoConsole(hPC);

            // Clean-up the pipes
            if (INVALID_HANDLE_VALUE != hPipeOut) CloseHandle(hPipeOut);
            if (INVALID_HANDLE_VALUE != hPipeIn) CloseHandle(hPipeIn);
        }
    }

    return S_OK == hr ? EXIT_SUCCESS : EXIT_FAILURE;
}

HRESULT CreatePseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut)
{
    HRESULT hr{ E_UNEXPECTED };
    HANDLE hPipePTYIn{ INVALID_HANDLE_VALUE };
    HANDLE hPipePTYOut{ INVALID_HANDLE_VALUE };

    // Create the pipes to which the ConPTY will connect
    if (CreatePipe(&hPipePTYIn, phPipeOut, NULL, 0) &&
        CreatePipe(phPipeIn, &hPipePTYOut, NULL, 0))
    {
        // Determine required size of Pseudo Console
        COORD consoleSize{};
        CONSOLE_SCREEN_BUFFER_INFO csbi{};
        HANDLE hConsole{ GetStdHandle(STD_OUTPUT_HANDLE) };
        if (GetConsoleScreenBufferInfo(hConsole, &csbi))
        {
            consoleSize.X = csbi.srWindow.Right - csbi.srWindow.Left + 1;
            consoleSize.Y = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
        }

        // Create the Pseudo Console of the required size, attached to the PTY-end of the pipes
        hr = CreatePseudoConsole(consoleSize, hPipePTYIn, hPipePTYOut, 0, phPC);

        // Note: We can close the handles to the PTY-end of the pipes here
        // because the handles are dup'ed into the ConHost and will be released
        // when the ConPTY is destroyed.
        if (INVALID_HANDLE_VALUE != hPipePTYOut) CloseHandle(hPipePTYOut);
        if (INVALID_HANDLE_VALUE != hPipePTYIn) CloseHandle(hPipePTYIn);
    }

    return hr;
}

// Initializes the specified startup info struct with the required properties and
// updates its thread attribute list with the specified ConPTY handle
HRESULT InitializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX* pStartupInfo, HPCON hPC)
{
    HRESULT hr{ E_UNEXPECTED };

    if (pStartupInfo)
    {
        SIZE_T attrListSize{};

        pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX);

        // Get the size of the thread attribute list.
        InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize);

        // Allocate a thread attribute list of the correct size
        pStartupInfo->lpAttributeList =
            reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(malloc(attrListSize));

        // Initialize thread attribute list
        if (pStartupInfo->lpAttributeList
            && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize))
        {
            // Set Pseudo Console attribute
            hr = UpdateProcThreadAttribute(
                pStartupInfo->lpAttributeList,
                0,
                PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
                hPC,
                sizeof(HPCON),
                NULL,
                NULL)
                ? S_OK
                : HRESULT_FROM_WIN32(GetLastError());
        }
        else
        {
            hr = HRESULT_FROM_WIN32(GetLastError());
        }
    }
    return hr;
}

void __cdecl PipeListener(LPVOID pipe)
{
    HANDLE hPipe{ pipe };
    HANDLE hConsole{ GetStdHandle(STD_OUTPUT_HANDLE) };

    const DWORD BUFF_SIZE{ 512 };
    char szBuffer[BUFF_SIZE]{};

    DWORD dwBytesWritten{};
    DWORD dwBytesRead{};
    BOOL fRead{ FALSE };
    do
    {
        // Read from the pipe
        fRead = ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwBytesRead, NULL);

        // Write received text to the Console
        // Note: Write to the Console using WriteFile(hConsole...), not printf()/puts() to
        // prevent partially-read VT sequences from corrupting output
        WriteFile(hConsole, szBuffer, dwBytesRead, &dwBytesWritten, NULL);

    } while (fRead && dwBytesRead >= 0);
}


More information about the Cygwin mailing list