//----------------------------------------------------------------------------
//
// Console input and output.
//
// Copyright (C) Microsoft Corporation, 1999-2000.
//
//----------------------------------------------------------------------------

#include "pch.cpp"
#pragma hdrstop

#include <stdarg.h>
#include <process.h>

#include "conio.hpp"
#include "engine.hpp"
#include "main.hpp"

#define CONTROL_A   1
#define CONTROL_B   2
#define CONTROL_D   4
#define CONTROL_E   5
#define CONTROL_F   6
#define CONTROL_K   11
#define CONTROL_P   16
#define CONTROL_R   18
#define CONTROL_V   22
#define CONTROL_W   23
#define CONTROL_X   24

HANDLE g_ConInput, g_ConOutput;
HANDLE g_PromptInput;
HANDLE g_AllowInput;

ConInputCallbacks g_ConInputCb;
ConOutputCallbacks g_ConOutputCb;

BOOL g_ConInitialized;
char g_Buffer[MAX_COMMAND];
LONG g_Lines;

HANDLE g_PipeWrite;
OVERLAPPED g_PipeWriteOverlapped;

CRITICAL_SECTION g_InputLock;
BOOL g_InputStarted;

// Input thread interfaces for direct input thread calls.
IDebugClient* g_ConClient;
IDebugControl* g_ConControl;

//----------------------------------------------------------------------------
//
// Default input callbacks implementation, provides IUnknown.
//
//----------------------------------------------------------------------------

STDMETHODIMP
DefInputCallbacks::QueryInterface(
    THIS_
    IN REFIID InterfaceId,
    OUT PVOID* Interface
    )
{
    *Interface = NULL;

    if (IsEqualIID(InterfaceId, IID_IUnknown) ||
        IsEqualIID(InterfaceId, IID_IDebugInputCallbacks))
    {
        *Interface = (IDebugInputCallbacks *)this;
        AddRef();
        return S_OK;
    }
    else
    {
        return E_NOINTERFACE;
    }
}

STDMETHODIMP_(ULONG)
DefInputCallbacks::AddRef(
    THIS
    )
{
    // This class is designed to be static so
    // there's no true refcount.
    return 1;
}

STDMETHODIMP_(ULONG)
DefInputCallbacks::Release(
    THIS
    )
{
    // This class is designed to be static so
    // there's no true refcount.
    return 0;
}

//----------------------------------------------------------------------------
//
// Console input callbacks.
//
//----------------------------------------------------------------------------

STDMETHODIMP
ConInputCallbacks::StartInput(
    THIS_
    IN ULONG BufferSize
    )
{
    if (g_IoMode == IO_NONE)
    {
        // Ignore input requests.
        return S_OK;
    }
    
    EnterCriticalSection(&g_InputLock);

    if (g_ConControl == NULL)
    {
        // If we're not remoted we aren't running a separate input
        // thread so we need to block here until we get some input.
        while (!ConIn(g_Buffer, sizeof(g_Buffer), TRUE))
        {
            ; // Wait.
        }
        g_DbgControl->ReturnInput(g_Buffer);
    }
    else if (ConIn(g_Buffer, sizeof(g_Buffer), FALSE))
    {
        g_ConControl->ReturnInput(g_Buffer);
    }
    else
    {
        g_InputStarted = TRUE;
        
#ifndef KERNEL
        // Wake up the input thread if necessary.
        SetEvent(g_AllowInput);
#endif
    }

    LeaveCriticalSection(&g_InputLock);
    return S_OK;
}

STDMETHODIMP
ConInputCallbacks::EndInput(
    THIS
    )
{
    g_InputStarted = FALSE;
    return S_OK;
}

//----------------------------------------------------------------------------
//
// Default output callbacks implementation, provides IUnknown.
//
//----------------------------------------------------------------------------

STDMETHODIMP
DefOutputCallbacks::QueryInterface(
    THIS_
    IN REFIID InterfaceId,
    OUT PVOID* Interface
    )
{
    *Interface = NULL;

    if (IsEqualIID(InterfaceId, IID_IUnknown) ||
        IsEqualIID(InterfaceId, IID_IDebugOutputCallbacks))
    {
        *Interface = (IDebugOutputCallbacks *)this;
        AddRef();
        return S_OK;
    }
    else
    {
        return E_NOINTERFACE;
    }
}

STDMETHODIMP_(ULONG)
DefOutputCallbacks::AddRef(
    THIS
    )
{
    // This class is designed to be static so
    // there's no true refcount.
    return 1;
}

STDMETHODIMP_(ULONG)
DefOutputCallbacks::Release(
    THIS
    )
{
    // This class is designed to be static so
    // there's no true refcount.
    return 0;
}

//----------------------------------------------------------------------------
//
// Console output callbacks.
//
//----------------------------------------------------------------------------

STDMETHODIMP
ConOutputCallbacks::Output(
    THIS_
    IN ULONG Mask,
    IN PCSTR Text
    )
{
    ConOutStr(Text);
    return S_OK;
}

//----------------------------------------------------------------------------
//
// Functions
//
//----------------------------------------------------------------------------

void
InitializeIo(PCSTR InputFile)
{
    __try
    {
        InitializeCriticalSection(&g_InputLock);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        ErrorExit("Unable to initialize lock\n");
    }
    
    // The input file may not exist so there's no
    // check for failure.
    g_InputFile = fopen(InputFile, "r");
}
    
void
CreateConsole(void)
{
    if (g_ConInitialized)
    {
        return;
    }
    
    // Set this early to prevent an init call from Exit in
    // case an Exit call is made inside this routine.
    g_ConInitialized = TRUE;
    
#ifdef INHERIT_CONSOLE
    
    g_ConInput = GetStdHandle(STD_INPUT_HANDLE);
    g_ConOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    
#else

    SECURITY_ATTRIBUTES Security;
    
    if (!AllocConsole())
    {
        ErrorExit("AllocConsole failed, %d\n", GetLastError());
    }

    ZeroMemory(&Security, sizeof(Security));
    Security.nLength = sizeof(Security);
    Security.bInheritHandle = TRUE;

    g_ConInput = CreateFile( "CONIN$",
                             GENERIC_READ,
                             FILE_SHARE_READ | FILE_SHARE_WRITE,
                             &Security,
                             OPEN_EXISTING,
                             0,
                             NULL
                             );
    if (g_ConInput == INVALID_HANDLE_VALUE)
    {
        ErrorExit("Create CONIN$ failed, %d\n", GetLastError());
    }

    g_ConOutput = CreateFile( "CONOUT$",
                              GENERIC_WRITE | GENERIC_READ,
                              FILE_SHARE_READ | FILE_SHARE_WRITE,
                              &Security,
                              OPEN_EXISTING,
                              0,
                              NULL
                              );
    if (g_ConOutput == INVALID_HANDLE_VALUE)
    {
        ErrorExit("Create CONOUT$ failed, %d\n", GetLastError());
    }
    
#endif

    g_PromptInput = g_ConInput;
}

void
ReadPromptInputChars(PSTR Buffer, ULONG BufferSize)
{
    ULONG Len;
    ULONG Read;
    
    // Reading from another source.  Read character by
    // character until a line is read.
    Len = 0;
    while (Len < BufferSize)
    {
        if (!ReadFile(g_PromptInput, &Buffer[Len], sizeof(Buffer[0]),
                      &Read, NULL) ||
            Read != sizeof(Buffer[0]))
        {
            OutputDebugString("Unable to read input\n");
            ExitDebugger(E_FAIL);
        }
                
        if (Buffer[Len] == '\n')
        {
            InterlockedDecrement(&g_Lines);
            break;
        }

        // Ignore carriage returns.
        if (Buffer[Len] != '\r')
        {
            // Prevent buffer overflow.
            if (Len == BufferSize - 1)
            {
                break;
            }
                    
            Len++;
        }
    }

    Buffer[Len] = '\0';
}

BOOL
CheckForControlCommands(PDEBUG_CLIENT Client, PDEBUG_CONTROL Control,
                        char Char)
{
    HRESULT Hr;
    ULONG OutMask;
    PCHAR DebugAction;
    ULONG EngOptions;
    
    switch(Char)
    {
    case CONTROL_B:
    case CONTROL_X:
        if (!g_RemoteClient && Client != NULL)
        {
            // Force servers to get cleaned up.
            Client->EndSession(DEBUG_END_REENTRANT);
        }
        ExitProcess(S_OK);

    case CONTROL_F:
        //
        // Force a breakin like Ctrl-C would do.
        // The advantage is this will work when kd is being debugged.
        //

        Control->SetInterrupt(DEBUG_INTERRUPT_ACTIVE);
        return TRUE;

    case CONTROL_P:
        // Launch cdb on this debugger.
        char PidStr[32];
        sprintf(PidStr, "\"cdb -p %d\"", GetCurrentProcessId());
        _spawnlp(_P_NOWAIT,
                 "cmd.exe", "/c", "start",
                 "remote", "/s", PidStr, "cdb_pipe",
                 NULL);
        return TRUE;

    case CONTROL_V:
        Client->GetOtherOutputMask(g_DbgClient, &OutMask);
        OutMask ^= DEBUG_OUTPUT_VERBOSE;
        Client->SetOtherOutputMask(g_DbgClient, OutMask);
        Control->SetLogMask(OutMask);
        ConOut("Verbose mode %s.\n",
               (OutMask & DEBUG_OUTPUT_VERBOSE) ? "ON" : "OFF");
        return TRUE;

    case CONTROL_W:
        Hr = Control->OutputVersionInformation(DEBUG_OUTCTL_AMBIENT);
        if (Hr == HRESULT_FROM_WIN32(ERROR_BUSY))
        {
            ConOut("Engine is busy, try again\n");
        }
        else if (Hr != S_OK)
        {
            ConOut("Unable to show version information, 0x%X\n", Hr);
        }
        return TRUE;

#ifdef KERNEL
            
    case CONTROL_A:
        Client->SetKernelConnectionOptions("cycle_speed");
        return TRUE;

    case CONTROL_D:
        Client->GetOtherOutputMask(g_DbgClient, &OutMask);
        OutMask ^= DEBUG_IOUTPUT_KD_PROTOCOL;
        Client->SetOtherOutputMask(g_DbgClient, OutMask);
        Control->SetLogMask(OutMask);
        return TRUE;

    case CONTROL_K:
        //
        // Toggle between the following possibilities-
        //
        // (0) no breakin
        // (1) -b style (same as Control-C up the wire)
        // (2) -d style (stop on first dll load).
        //
        // NB -b and -d could both be on the command line
        // but become mutually exclusive via this method.
        // (Maybe should be a single enum type).
        //

        Control->GetEngineOptions(&EngOptions);
        if (EngOptions & DEBUG_ENGOPT_INITIAL_BREAK)
        {
            //
            // Was type 1, go to type 2.
            //

            EngOptions |= DEBUG_ENGOPT_INITIAL_MODULE_BREAK;
            EngOptions &= ~DEBUG_ENGOPT_INITIAL_BREAK;

            DebugAction = "breakin on first symbol load";
        }
        else if (EngOptions & DEBUG_ENGOPT_INITIAL_MODULE_BREAK)
        {
            //
            // Was type 2, go to type 0.
            //
            
            EngOptions &= ~DEBUG_ENGOPT_INITIAL_MODULE_BREAK;
            DebugAction = "NOT breakin";
        }
        else
        {
            //
            // Was type 0, go to type 1.
            //

            EngOptions |= DEBUG_ENGOPT_INITIAL_BREAK;
            DebugAction = "request initial breakpoint";
        }
        Control->SetEngineOptions(EngOptions);
        ConOut("Will %s at next boot.\n", DebugAction);
        return TRUE;

    case CONTROL_R:
        Client->SetKernelConnectionOptions("resync");
        return TRUE;

#endif // #ifdef KERNEL
    }

    return FALSE;
}

BOOL
ConIn(PSTR Buffer, ULONG BufferSize, BOOL Wait)
{
    if (g_InitialCommand != NULL)
    {
        ConOut("%s: Reading initial command '%s'\n",
               g_DebuggerName, g_InitialCommand);
        strncpy(Buffer, g_InitialCommand, BufferSize);
        Buffer[BufferSize - 1] = 0;
        g_InitialCommand = NULL;
        return TRUE;
    }

    while (g_InputFile && g_InputFile != stdin)
    {
        if (fgets(Buffer, BufferSize, g_InputFile))
        {
            ConOut("%s", Buffer);
            Buffer[strlen(Buffer) - 1] = 0;
            return TRUE;
        }
        else
        {
            fclose(g_InputFile);
            if (g_NextOldInputFile > 0)
            {
                g_InputFile = g_OldInputFiles[--g_NextOldInputFile];
            }
            else
            {
                g_InputFile = stdin;
            }
        }
    }
    if (g_InputFile == NULL)
    {
        g_InputFile = stdin;
    }

    switch(g_IoMode)
    {
    case IO_NONE:
        return FALSE;
        
    case IO_DEBUG:
        if (!Wait)
        {
            return FALSE;
        }
        
        g_NtDllCalls.DbgPrompt("", Buffer,
                               min(BufferSize, MAX_DBG_PROMPT_COMMAND));
        break;

    case IO_CONSOLE:
        ULONG Len;
    
        if (g_PromptInput == g_ConInput)
        {
            if (!Wait)
            {
                return FALSE;
            }
            
            // Reading from the console so we can assume we'll
            // read a line.
            for (;;)
            {
                if (!ReadFile(g_PromptInput, Buffer, BufferSize, &Len, NULL))
                {
                    OutputDebugString("Unable to read input\n");
                    ExitDebugger(E_FAIL);
                }

                // At a minimum a read should have CRLF.  If it
                // doesn't assume that something weird happened
                // and ignore the read.
                if (Len >= 2)
                {
                    break;
                }

                Sleep(50);
            }
        
            // Remove CR LF.
            Len -= 2;
            Buffer[Len] = '\0';

            // Edit out any special characters.
            for (ULONG i = 0; i < Len; i++)
            {
                if (CheckForControlCommands(g_DbgClient, g_DbgControl,
                                            Buffer[i]))
                {
                    Buffer[i] = ' ';
                }
            }
        }
        else
        {
#ifndef KERNEL
            if (g_Lines == 0)
            {
                // Allow the input thread to read the console.
                SetEvent(g_AllowInput);
            }
#endif

            while (g_Lines == 0)
            {
                if (!Wait)
                {
                    return FALSE;
                }
                
                // Wait for the input thread to notify us that
                // a line of input is available.  While we're waiting,
                // let the engine process callbacks provoked by
                // other clients.
                HRESULT Hr = g_DbgClient->DispatchCallbacks(INFINITE);
                if (Hr != S_OK)
                {
                    OutputDebugString("Unable to dispatch callbacks\n");
                    ExitDebugger(Hr);
                }

                // Some other client may have started execution in
                // which case we want to stop waiting for input.
                if (g_ExecStatus != DEBUG_STATUS_BREAK)
                {
#ifndef KERNEL
                    // XXX drewb - Need a way to turn input off.
#endif
                    
                    return FALSE;
                }
            }

            ReadPromptInputChars(Buffer, BufferSize);
        }
        break;
    }

    return TRUE;
}

void
ConOutStr(PCSTR Str)
{
    switch(g_IoMode)
    {
    case IO_NONE:
        // Throw it away.
        break;

    case IO_DEBUG:
        //
        // Send the output to the kernel debugger but note that we
        // want any control C processing to be done locally rather
        // than in the kernel.
        //

        if (g_NtDllCalls.DbgPrint("%s", Str) == STATUS_BREAKPOINT &&
            g_DbgControl != NULL)
        {
            g_DbgControl->SetInterrupt(DEBUG_INTERRUPT_PASSIVE);
        }
        break;

    case IO_CONSOLE:
        if (g_ConOutput != NULL)
        {
            ULONG Written;
            WriteFile(g_ConOutput, Str, strlen(Str), &Written, NULL);
        }
        else
        {
            OutputDebugString(Str);
        }
        break;
    }
}

void
ConOut(PCSTR Format, ...)
{
    DWORD Len;
    va_list Args;
    
    // If no attempt has been made to create a console
    // go ahead and try now.
    if (g_IoMode == IO_CONSOLE && !g_ConInitialized)
    {
        CreateConsole();
    }
    
    va_start(Args, Format);
    Len = _vsnprintf(g_Buffer, sizeof(g_Buffer), Format, Args);
    if (Len == -1)
    {
        Len = sizeof(g_Buffer);
        g_Buffer[sizeof(g_Buffer) - 1] = 0;
    }
    va_end(Args);

    ConOutStr(g_Buffer);
}

void
ExitDebugger(ULONG Code)
{
    if (g_DbgClient != NULL && !g_RemoteClient)
    {
        g_DbgClient->EndSession(DEBUG_END_PASSIVE);
        // Force servers to get cleaned up.
        g_DbgClient->EndSession(DEBUG_END_REENTRANT);
    }

    ExitProcess(Code);
}

void
ErrorExit(PCSTR Format, ...)
{
    if (Format != NULL)
    {
        DWORD Len;
        va_list Args;

        // If no attempt has been made to create a console
        // go ahead and try now.
        if (g_IoRequested == IO_CONSOLE && !g_ConInitialized)
        {
            CreateConsole();
        }
            
        va_start(Args, Format);
        Len = _vsnprintf(g_Buffer, sizeof(g_Buffer), Format, Args);
        va_end(Args);

        if (g_ConOutput != NULL)
        {
            WriteFile(g_ConOutput, g_Buffer, Len, &Len, NULL);
        }
        else
        {
            OutputDebugString(g_Buffer);
        }
    }

#ifndef INHERIT_CONSOLE
    if (g_IoRequested == IO_CONSOLE)
    {
        ConOut("%s: exiting - press enter ---", g_DebuggerName);
        g_InitialCommand = NULL;
        g_InputFile = NULL;
        ConIn(g_Buffer, sizeof(g_Buffer), TRUE);
    }
#endif
    
    ExitDebugger(E_FAIL);
}

DWORD WINAPI
InputThreadLoop(PVOID Param)
{
    DWORD Read;
    BOOL Status;
    UCHAR Char;
    BOOL NewLine = TRUE;
    BOOL SpecialChar = FALSE;
    HANDLE ConIn = g_ConInput;
    HRESULT Hr;
    BOOL ShowInputError = TRUE;

    // Create interfaces usable on this thread.
    if ((Hr = g_DbgClient->CreateClient(&g_ConClient) != S_OK) ||
        (Hr = g_ConClient->QueryInterface(IID_IDebugControl,
                                          (void **)&g_ConControl)) != S_OK)
    {
        ConOut("%s: Unable to create input thread interfaces, 0x%X\n",
               g_DebuggerName, Hr);
        if (!g_RemoteClient)
        {
            // Force servers to get cleaned up.
            g_DbgClient->EndSession(DEBUG_END_REENTRANT);
        }
        ExitProcess(E_FAIL);
    }
    
    //
    // Capture all typed input immediately.
    // Stuff the characters into an anonymous pipe, from which
    // ConIn will read them.
    //

    for (;;)
    {
#ifndef KERNEL
        // The debugger should only read the console when the
        // debuggee isn't running to avoid eating up input 
        // intended for the debuggee.
        if (!g_RemoteClient && NewLine)
        {
            if (WaitForSingleObject(g_AllowInput,
                                    INFINITE) != WAIT_OBJECT_0)
            {
                ConOut("%s: Failed to wait for input window, %d\n",
                       GetLastError());
            }
            
            NewLine = FALSE;
        }
#endif
        
        Status = ReadFile(ConIn, &Char, sizeof(Char), &Read, NULL);
        if (!Status || Read != sizeof(Char))
        {
            if (ShowInputError &&
                GetLastError() != ERROR_OPERATION_ABORTED &&
                GetLastError() != ERROR_IO_PENDING)
            {
                ConOut("%s: Could not read from console, %d\n",
                       g_DebuggerName, GetLastError());
                ShowInputError = FALSE;
            }

            // The most common cause of a console read failure
            // is killing remote with @K.  Give things some
            // time to kill this process.
            // If this is a remote server it's possible that
            // the debugger was run without a valid console
            // and is just being accessed via remoting.
            // Sleep longer in that case since errors will
            // probably always occur.
            Sleep(!g_RemoteClient && g_RemoteOptions != NULL ?
                  1000 : 50);
            continue;
        }

        // We successfully got some input so if it
        // fails later we should show a fresh error.
        ShowInputError = TRUE;
        
        if (CheckForControlCommands(g_ConClient, g_ConControl, Char))
        {
            SpecialChar = TRUE;
            continue;
        }
            
        if (SpecialChar && Char == '\r')
        {
            // If we get a CR immediately after a special
            // char turn it into a space so that it doesn't cause
            // a command repeat.
            Char = ' ';
        }
            
        SpecialChar = FALSE;

        ULONG Len;
        Status = WriteFile(g_PipeWrite, &Char, sizeof(Char), &Len,
                           &g_PipeWriteOverlapped);
        if (!Status && GetLastError() != ERROR_IO_PENDING)
        {
            ConOut("%s: Could not write to pipe, %d\n",
                   g_DebuggerName, GetLastError());
        }
        else if (Char == '\n')
        {
            EnterCriticalSection(&g_InputLock);
            
            InterlockedIncrement(&g_Lines);
                
            // If input is needed send it directly
            // to the engine.
            if (g_InputStarted)
            {
                ReadPromptInputChars(g_Buffer, sizeof(g_Buffer));
                g_ConControl->ReturnInput(g_Buffer);
                g_InputStarted = FALSE;
            }
            else
            {
                // Wake up the engine thread when a line of
                // input is present.
                g_ConClient->ExitDispatch(g_DbgClient);
            }
            
            LeaveCriticalSection(&g_InputLock);
                
            NewLine = TRUE;
        }
    }

    return 0;
}

void
CreateInputThread(void)
{
    HANDLE Thread;
    DWORD ThreadId;
    CHAR PipeName[256];

    if (g_PipeWrite != NULL)
    {
        // Input thread already exists.
        return;
    }

#ifndef KERNEL
    g_AllowInput = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (g_AllowInput == NULL)
    {
        ErrorExit("Unable to create input event, %d\n", GetLastError());
    }
#endif
    
    _snprintf(PipeName,
              sizeof(PipeName),
              "\\\\.\\pipe\\Dbg%d",
              GetCurrentProcessId());
    g_PipeWrite = CreateNamedPipe(PipeName,
                                  PIPE_ACCESS_DUPLEX |
                                  FILE_FLAG_OVERLAPPED,
                                  PIPE_TYPE_BYTE | PIPE_READMODE_BYTE |
                                  PIPE_WAIT,
                                  2,
                                  2000,
                                  2000,
                                  NMPWAIT_WAIT_FOREVER,
                                  NULL);
    if (g_PipeWrite == INVALID_HANDLE_VALUE)
    {
        ErrorExit("Failed to create input pipe, %d\n",
                  GetLastError());
    }

    g_PromptInput = CreateFile(PipeName,
                               GENERIC_READ,
                               FILE_SHARE_READ,
                               NULL,
                               OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL,
                               NULL);
    if (g_PromptInput == INVALID_HANDLE_VALUE)
    {
        ErrorExit("Failed to create read pipe, %d\n",
                  GetLastError());
    }

    Thread = CreateThread(NULL,
                          16000, // THREAD_STACK_SIZE
                          InputThreadLoop,
                          NULL,
                          THREAD_SET_INFORMATION,
                          &ThreadId);
    if (Thread == NULL)
    {
        ErrorExit("Failed to create input thread, %d\n",
                  GetLastError());
    }
    else
    {
        if (!SetThreadPriority(Thread, THREAD_PRIORITY_ABOVE_NORMAL))
        {
            ErrorExit("Failed to raise the input thread priority, %d\n",
                      GetLastError());
        }
    }

    CloseHandle(Thread);

    // Wait for thread initialization.  Callbacks are
    // already registered so we need to dispatch them
    // while waiting.
    while (g_ConControl == NULL)
    {
        g_DbgClient->DispatchCallbacks(50);
    }
}