// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "precomp.h"

#include "srvinit.h"

#include "dbcs.h"
#include "handle.h"
#include "registry.hpp"
#include "renderFontDefaults.hpp"

#include "ApiRoutines.h"

#include "../types/inc/GlyphWidth.hpp"

#include "..\server\Entrypoints.h"
#include "..\server\IoSorter.h"

#include "..\interactivity\inc\ServiceLocator.hpp"
#include "..\interactivity\base\ApiDetector.hpp"

#include "renderData.hpp"
#include "../renderer/base/renderer.hpp"

#pragma hdrstop

using namespace Microsoft::Console::Interactivity;
using namespace Microsoft::Console::Render;

const UINT CONSOLE_EVENT_FAILURE_ID = 21790;
const UINT CONSOLE_LPC_PORT_FAILURE_ID = 21791;

[[nodiscard]] HRESULT ConsoleServerInitialization(_In_ HANDLE Server, const ConsoleArguments* const args)
{
    Globals& Globals = ServiceLocator::LocateGlobals();

    try
    {
        Globals.pDeviceComm = new DeviceComm(Server);

        Globals.launchArgs = *args;

        Globals.uiOEMCP = GetOEMCP();
        Globals.uiWindowsCP = GetACP();

        Globals.pFontDefaultList = new RenderFontDefaults();

        FontInfo::s_SetFontDefaultList(Globals.pFontDefaultList);
    }
    CATCH_RETURN();

    // Removed allocation of scroll buffer here.
    return S_OK;
}

static bool s_IsOnDesktop()
{
    // Persist this across calls so we don't dig it out a whole bunch of times. Once is good enough for the system.
    static bool fAlreadyQueried = false;
    static bool fIsDesktop = false;

    if (!fAlreadyQueried)
    {
        Microsoft::Console::Interactivity::ApiLevel level;
        const NTSTATUS status = Microsoft::Console::Interactivity::ApiDetector::DetectNtUserWindow(&level);
        LOG_IF_NTSTATUS_FAILED(status);

        if (NT_SUCCESS(status))
        {
            switch (level)
            {
            case Microsoft::Console::Interactivity::ApiLevel::OneCore:
                fIsDesktop = false;
                break;
            case Microsoft::Console::Interactivity::ApiLevel::Win32:
                fIsDesktop = true;
                break;
            }
        }

        fAlreadyQueried = true;
    }

    return fIsDesktop;
}

[[nodiscard]] NTSTATUS SetUpConsole(_Inout_ Settings* pStartupSettings,
                                    _In_ DWORD TitleLength,
                                    _In_reads_bytes_(TitleLength) LPWSTR Title,
                                    _In_ LPCWSTR CurDir,
                                    _In_ LPCWSTR AppName)
{
    // We will find and locate all relevant preference settings and then create the console here.
    // The precedence order for settings is:
    // 0. Launch arguments passed on the commandline.
    // 1. STARTUPINFO settings
    // 2a. Shortcut/Link settings
    // 2b. Registry specific settings
    // 3. Registry default settings
    // 4. Hardcoded default settings
    // To establish this hierarchy, we will need to load the settings and apply them in reverse order.

    // 4. Initializing Settings will establish hardcoded defaults.
    // Set to reference of global console information since that's the only place we need to hold the settings.
    CONSOLE_INFORMATION& settings = ServiceLocator::LocateGlobals().getConsoleInformation();
    const auto& launchArgs = ServiceLocator::LocateGlobals().launchArgs;
    // 4b. On Desktop editions, we need to apply a series of Desktop-specific defaults that are better than the
    // ones from the constructor (which are great for OneCore systems.)
    if (s_IsOnDesktop())
    {
        settings.ApplyDesktopSpecificDefaults();
    }

    // Use the launch arguments to check if we're going to be started in pseudoconsole mode.
    // If we are, we don't want to load any user settings, because that could
    //      result in some strange rendering results in the end terminal.
    // Use the launch args because the VtIo hasn't been initialized yet.
    if (!launchArgs.InConptyMode())
    {
        // 3. Read the default registry values.
        Registry reg(&settings);
        reg.LoadGlobalsFromRegistry();
        reg.LoadDefaultFromRegistry();

        // 2. Read specific settings

        // Link is expecting the flags from the process to be in already, so apply that first
        settings.SetStartupFlags(pStartupSettings->GetStartupFlags());

        // We need to see if we were spawned from a link. If we were, we need to
        // call back into the shell to try to get all the console information from the link.
        ServiceLocator::LocateSystemConfigurationProvider()->GetSettingsFromLink(&settings, Title, &TitleLength, CurDir, AppName);

        // If we weren't started from a link, this will already be set.
        // If LoadLinkInfo couldn't find anything, it will remove the flag so we can dig in the registry.
        if (!(settings.IsStartupTitleIsLinkNameSet()))
        {
            reg.LoadFromRegistry(Title);
        }
    }
    else
    {
        // microsoft/terminal#1965 - Let's just always enable VT processing by
        // default for conpty clients. This prevents peculiar differences in
        // behavior between conhost and terminal applications when the user has
        // VirtualTerminalLevel=1 in their registry.
        // We want everyone to be using VT by default anyways, so this is a
        // strong nudge in that direction. If an application _doesn't_ want VT
        // processing, it's free to disable this setting, even in conpty mode.
        settings.SetVirtTermLevel(1);
    }

    // 1. The settings we were passed contains STARTUPINFO structure settings to be applied last.
    settings.ApplyStartupInfo(pStartupSettings);

    // 0. The settings passed in via commandline arguments. These should override anything else.
    settings.ApplyCommandlineArguments(launchArgs);

    // Validate all applied settings for correctness against final rules.
    settings.Validate();

    // As of the graphics refactoring to library based, all fonts are now DPI aware. Scaling is
    // performed at the Blt time for raster fonts.
    // Note that we can only declare our DPI awareness once per process launch.
    // Set the process's default dpi awareness context to PMv2 so that new top level windows
    // inherit their WM_DPICHANGED* broadcast mode (and more, like dialog scaling) from the thread.

    IHighDpiApi* pHighDpiApi = ServiceLocator::LocateHighDpiApi();
    if (pHighDpiApi)
    {
        // N.B.: There is no high DPI support on OneCore (non-UAP) systems.
        //       Instead of implementing a no-op interface, just skip all high
        //       DPI configuration if it is not supported. All callers into the
        //       high DPI API are in the Win32-specific interactivity DLL.
        if (!pHighDpiApi->SetProcessDpiAwarenessContext())
        {
            // Fallback to per-monitor aware V1 if the API isn't available.
            LOG_IF_FAILED(pHighDpiApi->SetProcessPerMonitorDpiAwareness());

            // Allow child dialogs (i.e. Properties and Find) to scale automatically based on DPI if we're currently DPI aware.
            // Note that we don't need to do this if we're PMv2.
            pHighDpiApi->EnablePerMonitorDialogScaling();
        }
    }

    //Save initial font name for comparison on exit. We want telemetry when the font has changed
    if (settings.IsFaceNameSet())
    {
        settings.SetLaunchFaceName(settings.GetFaceName(), LF_FACESIZE);
    }

    // Allocate console will read the global ServiceLocator::LocateGlobals().getConsoleInformation
    // for the settings we just set.
    NTSTATUS Status = CONSOLE_INFORMATION::AllocateConsole({ Title, TitleLength / sizeof(wchar_t) });
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    return STATUS_SUCCESS;
}

[[nodiscard]] NTSTATUS RemoveConsole(_In_ ConsoleProcessHandle* ProcessData)
{
    CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
    LockConsole();
    NTSTATUS Status = STATUS_SUCCESS;

    CommandHistory::s_Free((HANDLE)ProcessData);

    bool const fRecomputeOwner = ProcessData->fRootProcess;
    gci.ProcessHandleList.FreeProcessData(ProcessData);

    if (fRecomputeOwner)
    {
        Microsoft::Console::Types::IConsoleWindow* pWindow = ServiceLocator::LocateConsoleWindow();
        if (pWindow != nullptr)
        {
            pWindow->SetOwner();
        }
    }

    UnlockConsole();

    return Status;
}

DWORD WINAPI ConsoleIoThread(LPVOID lpParameter);

void ConsoleCheckDebug()
{
#ifdef DBG
    wil::unique_hkey hCurrentUser;
    wil::unique_hkey hConsole;
    NTSTATUS status = RegistrySerialization::s_OpenConsoleKey(&hCurrentUser, &hConsole);

    if (NT_SUCCESS(status))
    {
        DWORD dwData = 0;
        status = RegistrySerialization::s_QueryValue(hConsole.get(),
                                                     L"DebugLaunch",
                                                     sizeof(dwData),
                                                     REG_DWORD,
                                                     (BYTE*)&dwData,
                                                     nullptr);

        if (NT_SUCCESS(status))
        {
            if (dwData != 0)
            {
                DebugBreak();
            }
        }
    }
#endif
}

[[nodiscard]] HRESULT ConsoleCreateIoThreadLegacy(_In_ HANDLE Server, const ConsoleArguments* const args)
{
    auto& g = ServiceLocator::LocateGlobals();
    RETURN_IF_FAILED(ConsoleServerInitialization(Server, args));
    RETURN_IF_FAILED(g.consoleInputSetupEvent.create(wil::EventOptions::ManualReset));
    RETURN_IF_FAILED(g.consoleInputInitializedEvent.create(wil::EventOptions::ManualReset));

    // Set up and tell the driver about the input available event.
    RETURN_IF_FAILED(g.hInputEvent.create(wil::EventOptions::ManualReset));

    CD_IO_SERVER_INFORMATION ServerInformation;
    ServerInformation.InputAvailableEvent = ServiceLocator::LocateGlobals().hInputEvent.get();
    RETURN_IF_FAILED(g.pDeviceComm->SetServerInformation(&ServerInformation));

    HANDLE const hThread = CreateThread(nullptr, 0, ConsoleIoThread, 0, 0, nullptr);
    RETURN_HR_IF(E_HANDLE, hThread == nullptr);
    LOG_IF_WIN32_BOOL_FALSE(CloseHandle(hThread)); // The thread will run on its own and close itself. Free the associated handle.

    // See MSFT:19918626
    // Make sure to always set up the signal thread if we need to.
    // Do this first, because breaking the signal pipe is used by the conpty API
    //  to indicate that we should close.
    // The conpty i/o threads need an actual client to be connected before they
    //      can start, so they're started below, in ConsoleAllocateConsole
    auto& gci = g.getConsoleInformation();
    RETURN_IF_FAILED(gci.GetVtIo()->Initialize(args));
    RETURN_IF_FAILED(gci.GetVtIo()->CreateAndStartSignalThread());

    return S_OK;
}

#define SYSTEM_ROOT (L"%SystemRoot%")
#define SYSTEM_ROOT_LENGTH (sizeof(SYSTEM_ROOT) - sizeof(WCHAR))

// Routine Description:
// - This routine translates path characters into '_' characters because the NT registry apis do not allow the creation of keys with
//   names that contain path characters. It also converts absolute paths into %SystemRoot% relative ones. As an example, if both behaviors were
//   specified it would convert a title like C:\WINNT\System32\cmd.exe to %SystemRoot%_System32_cmd.exe.
// Arguments:
// - ConsoleTitle - Pointer to string to translate.
// - Unexpand - Convert absolute path to %SystemRoot% relative one.
// - Substitute - Whether string-substitution ('_' for '\') should occur.
// Return Value:
// - Pointer to translated title or nullptr.
// Note:
// - This routine allocates a buffer that must be freed.
PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand, const BOOL fSubstitute)
{
    LPWSTR Tmp = nullptr;

    size_t cbConsoleTitle;
    size_t cbSystemRoot;

    LPWSTR pwszSysRoot = new (std::nothrow) wchar_t[MAX_PATH];
    if (nullptr != pwszSysRoot)
    {
        if (0 != GetWindowsDirectoryW(pwszSysRoot, MAX_PATH))
        {
            if (SUCCEEDED(StringCbLengthW(pwszConsoleTitle, STRSAFE_MAX_CCH, &cbConsoleTitle)) &&
                SUCCEEDED(StringCbLengthW(pwszSysRoot, MAX_PATH, &cbSystemRoot)))
            {
                int const cchSystemRoot = (int)(cbSystemRoot / sizeof(WCHAR));
                int const cchConsoleTitle = (int)(cbConsoleTitle / sizeof(WCHAR));
                cbConsoleTitle += sizeof(WCHAR); // account for nullptr terminator

                if (fUnexpand &&
                    cchConsoleTitle >= cchSystemRoot &&
#pragma prefast(suppress : 26018, "We've guaranteed that cchSystemRoot is equal to or smaller than cchConsoleTitle in size.")
                    (CSTR_EQUAL == CompareStringOrdinal(pwszConsoleTitle, cchSystemRoot, pwszSysRoot, cchSystemRoot, TRUE)))
                {
                    cbConsoleTitle -= cbSystemRoot;
                    pwszConsoleTitle += cchSystemRoot;
                    cbSystemRoot = SYSTEM_ROOT_LENGTH;
                }
                else
                {
                    cbSystemRoot = 0;
                }

                LPWSTR pszTranslatedConsoleTitle;
                const size_t cbTranslatedConsoleTitle = cbSystemRoot + cbConsoleTitle;
                Tmp = pszTranslatedConsoleTitle = (PWSTR) new BYTE[cbTranslatedConsoleTitle];
                if (pszTranslatedConsoleTitle == nullptr)
                {
                    return nullptr;
                }

                // No need to check return here -- pszTranslatedConsoleTitle is guaranteed large enough for SYSTEM_ROOT
                (void)StringCbCopy(pszTranslatedConsoleTitle, cbTranslatedConsoleTitle, SYSTEM_ROOT);
                pszTranslatedConsoleTitle += (cbSystemRoot / sizeof(WCHAR)); // skip by characters -- not bytes

                for (UINT i = 0; i < cbConsoleTitle; i += sizeof(WCHAR))
                {
#pragma prefast(suppress : 26018, "We are reading the null portion of the buffer on purpose and will escape on reaching it below.")
                    if (fSubstitute && *pwszConsoleTitle == '\\')
                    {
#pragma prefast(suppress : 26019, "Console title must contain system root if this path was followed.")
                        *pszTranslatedConsoleTitle++ = (WCHAR)'_';
                    }
                    else
                    {
                        *pszTranslatedConsoleTitle++ = *pwszConsoleTitle;
                        if (*pwszConsoleTitle == L'\0')
                        {
                            break;
                        }
                    }

                    pwszConsoleTitle++;
                }
            }
        }
        delete[] pwszSysRoot;
    }

    return Tmp;
}

[[nodiscard]] NTSTATUS GetConsoleLangId(const UINT uiOutputCP, _Out_ LANGID* const pLangId)
{
    NTSTATUS Status = STATUS_NOT_SUPPORTED;

    // -- WARNING -- LOAD BEARING CODE --
    // Only attempt to return the Lang ID if the Windows ACP on console launch was an East Asian Code Page.
    // -
    // As of right now, this is a load bearing check and causes a domino effect of errors during OEM preinstallation if removed
    // resulting in a crash on launch of CMD.exe
    // (and consequently any scripts OEMs use to customize an image during the auditUser preinstall step inside their unattend.xml files.)
    // I have no reason to believe that removing this check causes any problems on any other SKU or scenario types.
    // -
    // Returning STATUS_NOT_SUPPORTED will skip a call to SetThreadLocale inside the Windows loader. This has the effect of not
    // setting the appropriate locale on the client end of the pipe, but also avoids the error.
    // Returning STATUS_SUCCESS will trigger the call to SetThreadLocale inside the loader.
    // This method is called on process launch by the loader and on every SetConsoleOutputCP call made from the client application to
    // maintain the synchrony of the client's Thread Locale state.
    // -
    // It is important to note that a comment exists inside the loader stating that DBCS code pages (CJK languages)
    // must have the SetThreadLocale synchronized with the console in order for FormatMessage to output correctly.
    // I'm not sure of the full validity of that comment at this point in time (Nov 2016), but the least risky thing is to trust it and revert
    // the behavior to this function until it can be otherwise proven.
    // -
    // See MSFT: 9808579 for the complete story on what happened here and why this must stay until the other dominos are resolved.
    // -
    // I would also highly advise against expanding the LANGIDs returned here or modifying them in any way until the cascading impacts
    // discovered in MSFT: 9808579 are vetted against any changes.
    // -- END WARNING --
    if (IsAvailableEastAsianCodePage(ServiceLocator::LocateGlobals().uiWindowsCP))
    {
        if (pLangId != nullptr)
        {
            switch (uiOutputCP)
            {
            case CP_JAPANESE:
                *pLangId = MAKELANGID(LANG_JAPANESE, SUBLANG_DEFAULT);
                break;
            case CP_KOREAN:
                *pLangId = MAKELANGID(LANG_KOREAN, SUBLANG_KOREAN);
                break;
            case CP_CHINESE_SIMPLIFIED:
                *pLangId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED);
                break;
            case CP_CHINESE_TRADITIONAL:
                *pLangId = MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL);
                break;
            default:
                *pLangId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
                break;
            }
        }
        Status = STATUS_SUCCESS;
    }

    return Status;
}

[[nodiscard]] HRESULT ApiRoutines::GetConsoleLangIdImpl(LANGID& langId) noexcept
{
    try
    {
        const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
        LockConsole();
        auto Unlock = wil::scope_exit([&] { UnlockConsole(); });

        // This fails a lot and it's totally expected. It only works for a few East Asian code pages.
        // As such, just return it. Do NOT use a wil macro here. It is very noisy.
        return HRESULT_FROM_NT(GetConsoleLangId(gci.OutputCP, &langId));
    }
    CATCH_RETURN();
}

// Routine Description:
// - This routine reads the connection information from a 'connect' IO, validates it and stores them in an internal format.
// - N.B. The internal informat contains information not sent by clients in their connect IOs and intialized by other routines.
// Arguments:
// - Server - Supplies a handle to the console server.
// - Message - Supplies the message representing the connect IO.
// - Cac - Receives the connection information.
// Return Value:
// - NTSTATUS indicating if the connection information was successfully initialized.
[[nodiscard]] NTSTATUS ConsoleInitializeConnectInfo(_In_ PCONSOLE_API_MSG Message, _Out_ PCONSOLE_API_CONNECTINFO Cac)
{
    CONSOLE_SERVER_MSG Data = { 0 };
    // Try to receive the data sent by the client.
    NTSTATUS Status = NTSTATUS_FROM_HRESULT(Message->ReadMessageInput(0, &Data, sizeof(Data)));
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    // Validate that strings are within the buffers and null-terminated.
    if ((Data.ApplicationNameLength > (sizeof(Data.ApplicationName) - sizeof(WCHAR))) ||
        (Data.TitleLength > (sizeof(Data.Title) - sizeof(WCHAR))) ||
        (Data.CurrentDirectoryLength > (sizeof(Data.CurrentDirectory) - sizeof(WCHAR))) ||
        (Data.ApplicationName[Data.ApplicationNameLength / sizeof(WCHAR)] != UNICODE_NULL) ||
        (Data.Title[Data.TitleLength / sizeof(WCHAR)] != UNICODE_NULL) || (Data.CurrentDirectory[Data.CurrentDirectoryLength / sizeof(WCHAR)] != UNICODE_NULL))
    {
        return STATUS_INVALID_BUFFER_SIZE;
    }

    // Initialize (partially) the connect info with the received data.
    FAIL_FAST_IF(!(sizeof(Cac->AppName) == sizeof(Data.ApplicationName)));
    FAIL_FAST_IF(!(sizeof(Cac->Title) == sizeof(Data.Title)));
    FAIL_FAST_IF(!(sizeof(Cac->CurDir) == sizeof(Data.CurrentDirectory)));

    // unused(Data.IconId)
    Cac->ConsoleInfo.SetHotKey(Data.HotKey);
    Cac->ConsoleInfo.SetStartupFlags(Data.StartupFlags);
    Cac->ConsoleInfo.SetFillAttribute(Data.FillAttribute);
    Cac->ConsoleInfo.SetShowWindow(Data.ShowWindow);
    Cac->ConsoleInfo.SetScreenBufferSize(Data.ScreenBufferSize);
    Cac->ConsoleInfo.SetWindowSize(Data.WindowSize);
    Cac->ConsoleInfo.SetWindowOrigin(Data.WindowOrigin);
    Cac->ProcessGroupId = Data.ProcessGroupId;
    Cac->ConsoleApp = Data.ConsoleApp;
    Cac->WindowVisible = Data.WindowVisible;
    Cac->TitleLength = Data.TitleLength;
    Cac->AppNameLength = Data.ApplicationNameLength;
    Cac->CurDirLength = Data.CurrentDirectoryLength;

    memmove(Cac->AppName, Data.ApplicationName, sizeof(Cac->AppName));
    memmove(Cac->Title, Data.Title, sizeof(Cac->Title));
    memmove(Cac->CurDir, Data.CurrentDirectory, sizeof(Cac->CurDir));

    return STATUS_SUCCESS;
}

[[nodiscard]] NTSTATUS ConsoleAllocateConsole(PCONSOLE_API_CONNECTINFO p)
{
    // AllocConsole is outside our codebase, but we should be able to mostly track the call here.
    Telemetry::Instance().LogApiCall(Telemetry::ApiCall::AllocConsole);

    Globals& g = ServiceLocator::LocateGlobals();

    CONSOLE_INFORMATION& gci = g.getConsoleInformation();

    NTSTATUS Status = SetUpConsole(&p->ConsoleInfo, p->TitleLength, p->Title, p->CurDir, p->AppName);
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    // No matter what, create a renderer.
    try
    {
        g.pRender = nullptr;

        auto renderThread = std::make_unique<RenderThread>();
        // stash a local pointer to the thread here -
        // We're going to give ownership of the thread to the Renderer,
        //      but the thread also need to be told who its renderer is,
        //      and we can't do that until the renderer is constructed.
        auto* const localPointerToThread = renderThread.get();

        g.pRender = new Renderer(&gci.renderData, nullptr, 0, std::move(renderThread));

        THROW_IF_FAILED(localPointerToThread->Initialize(g.pRender));

        // Allow the renderer to paint.
        g.pRender->EnablePainting();

        // Set up the renderer to be used to calculate the width of a glyph,
        //      should we be unable to figure out its width another way.
        auto pfn = std::bind(&Renderer::IsGlyphWideByFont, static_cast<Renderer*>(g.pRender), std::placeholders::_1);
        SetGlyphWidthFallback(pfn);
    }
    catch (...)
    {
        Status = NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException());
    }

    if (NT_SUCCESS(Status) && p->WindowVisible)
    {
        HANDLE Thread = nullptr;

        IConsoleInputThread* pNewThread = nullptr;
        LOG_IF_FAILED(ServiceLocator::CreateConsoleInputThread(&pNewThread));

        FAIL_FAST_IF_NULL(pNewThread);

        Thread = pNewThread->Start();
        if (Thread == nullptr)
        {
            Status = STATUS_NO_MEMORY;
        }
        else
        {
            ServiceLocator::LocateGlobals().dwInputThreadId = pNewThread->GetThreadId();

            // The ConsoleInputThread needs to perform things under lock,
            // but if we unlock it, we don't know who will get the lock.
            // So we will signal that it is safe for the other thread to do its work as
            // we hold the lock on its behalf and wait for it to tell us that it is done.
            g.consoleInputSetupEvent.SetEvent();
            g.consoleInputInitializedEvent.wait();

            // OK, we've been told that the input thread is done initializing under lock.
            // Cleanup the handles and events we used to maintain our virtual lock passing dance.

            CloseHandle(Thread); // This doesn't stop the thread from running.

            g.consoleInputInitializedEvent.release();
            g.consoleInputSetupEvent.release();

            if (!NT_SUCCESS(g.ntstatusConsoleInputInitStatus))
            {
                Status = g.ntstatusConsoleInputInitStatus;
            }
            else
            {
                Status = STATUS_SUCCESS;
            }

            // If we're not headless, we'll make a real window.
            // Allow UI Access to the real window but not the little
            // fake window we would make in headless mode.
            if (!g.launchArgs.IsHeadless())
            {
                /*
                 * Tell driver to allow clients with UIAccess to connect
                 * to this server even if the security descriptor doesn't
                 * allow it.
                 *
                 * N.B. This allows applications like narrator.exe to have
                 *      access to the console. This is ok because they already
                 *      have access to the console window anyway - this function
                 *      is only called when a window is created.
                 */

                LOG_IF_FAILED(g.pDeviceComm->AllowUIAccess());
            }
        }
    }

    // Potentially start the VT IO (if needed)
    // Make sure to do this after the i/o buffers have been created.
    // We'll need the size of the screen buffer in the vt i/o initialization
    if (NT_SUCCESS(Status))
    {
        HRESULT hr = gci.GetVtIo()->CreateIoHandlers();
        if (hr == S_FALSE)
        {
            // We're not in VT I/O mode, this is fine.
        }
        else if (SUCCEEDED(hr))
        {
            // Actually start the VT I/O threads
            hr = gci.GetVtIo()->StartIfNeeded();
            // Don't convert S_FALSE to an NTSTATUS - the equivalent NTSTATUS
            //      is treated as an error
            if (hr != S_FALSE)
            {
                Status = NTSTATUS_FROM_HRESULT(hr);
            }
            else
            {
                Status = ERROR_SUCCESS;
            }
        }
        else
        {
            Status = NTSTATUS_FROM_HRESULT(hr);
        }
    }

    return Status;
}

// Routine Description:
// - This routine is the main one in the console server IO thread.
// - It reads IO requests submitted by clients through the driver, services and completes them in a loop.
// Arguments:
// - <none>
// Return Value:
// - This routine never returns. The process exits when no more references or clients exist.
DWORD WINAPI ConsoleIoThread(LPVOID /*lpParameter*/)
{
    auto& globals = ServiceLocator::LocateGlobals();

    CONSOLE_API_MSG ReceiveMsg;
    ReceiveMsg._pApiRoutines = &globals.api;
    ReceiveMsg._pDeviceComm = globals.pDeviceComm;
    PCONSOLE_API_MSG ReplyMsg = nullptr;

    bool fShouldExit = false;
    while (!fShouldExit)
    {
        if (ReplyMsg != nullptr)
        {
            LOG_IF_FAILED(ReplyMsg->ReleaseMessageBuffers());
        }

        // TODO: 9115192 correct mixed NTSTATUS/HRESULT
        HRESULT hr = ServiceLocator::LocateGlobals().pDeviceComm->ReadIo(ReplyMsg, &ReceiveMsg);
        if (FAILED(hr))
        {
            if (hr == HRESULT_FROM_WIN32(ERROR_PIPE_NOT_CONNECTED) ||
                hr == E_APPLICATION_EXITING)
            {
                fShouldExit = true;

                // This will not return. Terminate immediately when disconnected.
                ServiceLocator::RundownAndExit(STATUS_SUCCESS);
            }
            RIPMSG1(RIP_WARNING, "DeviceIoControl failed with Result 0x%x", hr);
            ReplyMsg = nullptr;
            continue;
        }

        IoSorter::ServiceIoOperation(&ReceiveMsg, &ReplyMsg);
    }

    return 0;
}
