932 lines
39 KiB
C++
932 lines
39 KiB
C++
// 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/DeviceHandle.h"
|
|
#include "../server/Entrypoints.h"
|
|
#include "../server/IoSorter.h"
|
|
|
|
#include "../interactivity/inc/ServiceLocator.hpp"
|
|
#include "../interactivity/base/ApiDetector.hpp"
|
|
#include "../interactivity/base/RemoteConsoleControl.hpp"
|
|
|
|
#include "renderData.hpp"
|
|
#include "../renderer/base/renderer.hpp"
|
|
|
|
#include "../inc/conint.h"
|
|
#include "../propslib/DelegationConfig.hpp"
|
|
|
|
#if TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED
|
|
#include "ITerminalHandoff.h"
|
|
#endif // TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED
|
|
|
|
#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)
|
|
try
|
|
{
|
|
Globals& Globals = ServiceLocator::LocateGlobals();
|
|
|
|
if (!Globals.pDeviceComm)
|
|
{
|
|
// in rare circumstances (such as in the fuzzing harness), there will already be a device comm
|
|
Globals.pDeviceComm = new ConDrvDeviceComm(Server);
|
|
}
|
|
|
|
Globals.launchArgs = *args;
|
|
|
|
Globals.uiOEMCP = GetOEMCP();
|
|
Globals.uiWindowsCP = GetACP();
|
|
|
|
Globals.pFontDefaultList = new RenderFontDefaults();
|
|
|
|
FontInfoBase::s_SetFontDefaultList(Globals.pFontDefaultList);
|
|
|
|
// Check if this conhost is allowed to delegate its activities to another.
|
|
// If so, look up the registered default console handler.
|
|
bool isEnabled = false;
|
|
if (SUCCEEDED(Microsoft::Console::Internal::DefaultApp::CheckDefaultAppPolicy(isEnabled)) && isEnabled)
|
|
{
|
|
IID delegationClsid;
|
|
if (SUCCEEDED(DelegationConfig::s_GetDefaultConsoleId(delegationClsid)))
|
|
{
|
|
Globals.handoffConsoleClsid = delegationClsid;
|
|
}
|
|
if (SUCCEEDED(DelegationConfig::s_GetDefaultTerminalId(delegationClsid)))
|
|
{
|
|
Globals.handoffTerminalClsid = delegationClsid;
|
|
}
|
|
}
|
|
|
|
// Create the accessibility notifier early in the startup process.
|
|
// Only create if we're not in PTY mode.
|
|
// The notifiers use expensive legacy MSAA events and the PTY isn't even responsible
|
|
// for the terminal user interface, so we should set ourselves up to skip all
|
|
// those notifications and the mathematical calculations required to send those events
|
|
// for performance reasons.
|
|
if (!args->InConptyMode())
|
|
{
|
|
RETURN_IF_FAILED(ServiceLocator::CreateAccessibilityNotifier());
|
|
}
|
|
|
|
// Removed allocation of scroll buffer here.
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
//Save initial font name for comparison on exit. We want telemetry when the font has changed
|
|
if (settings.IsFaceNameSet())
|
|
{
|
|
settings.SetLaunchFaceName(settings.GetFaceName());
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Sets up the main driver message packet (I/O) processing
|
|
// thread that will handle all client requests from all
|
|
// attached command-line applications for the duration
|
|
// of this console server session.
|
|
// - The optional arguments are only used when receiving a handoff
|
|
// from another console server (typically in-box to the Windows OS image)
|
|
// that has already started processing the console session.
|
|
// They will be blank and generated internally by this method if this is the first
|
|
// console server starting in response to a client startup or ConPTY setup
|
|
// request.
|
|
// Arguments:
|
|
// - Server - Handle to the console driver that represents
|
|
// our server side of the connection.
|
|
// - args - Command-line arguments from starting this console host
|
|
// that may affect the way we host the session.
|
|
// - driverInputEvent - (Optional) Event registered with the console driver
|
|
// that we will use to wake up input read requests that
|
|
// are blocked because they came in when we had no input ready.
|
|
// - connectMessage - (Optional) A message received from a connecting client
|
|
// by another console server that is being passed off to us as a part of
|
|
// the handoff strategy.
|
|
HRESULT ConsoleCreateIoThread(_In_ HANDLE Server,
|
|
const ConsoleArguments* const args,
|
|
HANDLE driverInputEvent,
|
|
PCONSOLE_API_MSG connectMessage)
|
|
{
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
RETURN_IF_FAILED(ConsoleServerInitialization(Server, args));
|
|
RETURN_IF_FAILED(g.hConsoleInputInitEvent.create(wil::EventOptions::None));
|
|
|
|
if (driverInputEvent != INVALID_HANDLE_VALUE)
|
|
{
|
|
// Store the driver input event. It's already been told that it exists by whomever started us.
|
|
g.hInputEvent.reset(driverInputEvent);
|
|
}
|
|
else
|
|
{
|
|
// 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));
|
|
}
|
|
|
|
// Ensure that whatever we're giving to the new thread is on the heap so it cannot
|
|
// go out of scope by the time that thread starts.
|
|
// (e.g. if someone sent us a pointer to stack memory... that could happen
|
|
// ask me how I know... :| )
|
|
std::unique_ptr<CONSOLE_API_MSG> heapConnectMessage;
|
|
if (connectMessage)
|
|
{
|
|
// Allocate and copy onto the heap
|
|
heapConnectMessage = std::make_unique<CONSOLE_API_MSG>(*connectMessage);
|
|
|
|
// Set the pointer that `CreateThread` uses to the heap space
|
|
connectMessage = heapConnectMessage.get();
|
|
}
|
|
|
|
HANDLE const hThread = CreateThread(nullptr, 0, ConsoleIoThread, connectMessage, 0, nullptr);
|
|
RETURN_HR_IF(E_HANDLE, hThread == nullptr);
|
|
|
|
// If we successfully started the other thread, it's that guy's problem to free the connect message.
|
|
// (If we didn't make one, it should be no problem to release the empty unique_ptr.)
|
|
heapConnectMessage.release();
|
|
|
|
LOG_IF_FAILED(SetThreadDescription(hThread, L"Console Driver Message IO Thread"));
|
|
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;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Accepts a console server session from another console server
|
|
// most commonly from the operating system in-box console to
|
|
// a more-up-to-date and out-of-band delivered one.
|
|
// Arguments:
|
|
// - Server - Handle to the console driver that represents our server
|
|
// side of hosting the console session
|
|
// - driverInputEvent - Handle to an event already registered with the
|
|
// driver that clients will implicitly wait on when we don't have
|
|
// any input to return in the queue when a request is made and is
|
|
// signaled to unblock them when input finally arrives.
|
|
// - connectMessage - A console driver/server message as received
|
|
// by the previous console server for us to finish processing in
|
|
// order to complete the client's initial connection and store
|
|
// all necessary callback information for all subsequent API calls.
|
|
// Return Value:
|
|
// - COM errors, registry errors, pipe errors, handle manipulation errors,
|
|
// errors from the creating the thread for the
|
|
// standard IO thread loop for the server to process messages
|
|
// from the driver... or an S_OK success.
|
|
[[nodiscard]] HRESULT ConsoleEstablishHandoff([[maybe_unused]] _In_ HANDLE Server,
|
|
[[maybe_unused]] HANDLE driverInputEvent,
|
|
[[maybe_unused]] HANDLE hostSignalPipe,
|
|
[[maybe_unused]] HANDLE hostProcessHandle,
|
|
[[maybe_unused]] PCONSOLE_API_MSG connectMessage)
|
|
try
|
|
{
|
|
#if !TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED
|
|
return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
|
|
#else // TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED
|
|
auto& g = ServiceLocator::LocateGlobals();
|
|
g.handoffTarget = true;
|
|
|
|
IID delegationClsid;
|
|
if (SUCCEEDED(DelegationConfig::s_GetDefaultConsoleId(delegationClsid)))
|
|
{
|
|
g.handoffConsoleClsid = delegationClsid;
|
|
}
|
|
if (SUCCEEDED(DelegationConfig::s_GetDefaultTerminalId(delegationClsid)))
|
|
{
|
|
g.handoffTerminalClsid = delegationClsid;
|
|
}
|
|
|
|
if (!g.handoffTerminalClsid)
|
|
{
|
|
return E_NOT_SET;
|
|
}
|
|
|
|
// Capture handle to the inbox process into a unique handle holder.
|
|
g.handoffInboxConsoleHandle.reset(hostProcessHandle);
|
|
|
|
// Set up a threadpool waiter to shutdown everything if the inbox process disappears.
|
|
g.handoffInboxConsoleExitWait.reset(CreateThreadpoolWait(
|
|
[](PTP_CALLBACK_INSTANCE /*callbackInstance*/, PVOID /*context*/, PTP_WAIT /*wait*/, TP_WAIT_RESULT /*waitResult*/) noexcept {
|
|
ServiceLocator::RundownAndExit(E_APPLICATION_MANAGER_NOT_RUNNING);
|
|
},
|
|
nullptr,
|
|
nullptr));
|
|
RETURN_LAST_ERROR_IF_NULL(g.handoffInboxConsoleExitWait.get());
|
|
|
|
SetThreadpoolWait(g.handoffInboxConsoleExitWait.get(), g.handoffInboxConsoleHandle.get(), nullptr);
|
|
|
|
std::unique_ptr<IConsoleControl> remoteControl = std::make_unique<Microsoft::Console::Interactivity::RemoteConsoleControl>(hostSignalPipe);
|
|
RETURN_IF_NTSTATUS_FAILED(ServiceLocator::SetConsoleControlInstance(std::move(remoteControl)));
|
|
|
|
wil::unique_handle signalPipeTheirSide;
|
|
wil::unique_handle signalPipeOurSide;
|
|
|
|
wil::unique_handle inPipeTheirSide;
|
|
wil::unique_handle inPipeOurSide;
|
|
|
|
wil::unique_handle outPipeTheirSide;
|
|
wil::unique_handle outPipeOurSide;
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeOurSide.addressof(), signalPipeTheirSide.addressof(), nullptr, 0));
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(inPipeOurSide.addressof(), inPipeTheirSide.addressof(), nullptr, 0));
|
|
|
|
RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(outPipeTheirSide.addressof(), outPipeOurSide.addressof(), nullptr, 0));
|
|
|
|
wil::unique_handle clientProcess{ OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, static_cast<DWORD>(connectMessage->Descriptor.Process)) };
|
|
RETURN_LAST_ERROR_IF_NULL(clientProcess.get());
|
|
|
|
wil::unique_handle refHandle;
|
|
RETURN_IF_NTSTATUS_FAILED(DeviceHandle::CreateClientHandle(refHandle.addressof(),
|
|
Server,
|
|
L"\\Reference",
|
|
FALSE));
|
|
|
|
const auto serverProcess = GetCurrentProcess();
|
|
|
|
::Microsoft::WRL::ComPtr<ITerminalHandoff> handoff;
|
|
|
|
RETURN_IF_FAILED(CoCreateInstance(g.handoffTerminalClsid.value(), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&handoff)));
|
|
|
|
RETURN_IF_FAILED(handoff->EstablishPtyHandoff(inPipeTheirSide.get(),
|
|
outPipeTheirSide.get(),
|
|
signalPipeTheirSide.get(),
|
|
refHandle.get(),
|
|
serverProcess,
|
|
clientProcess.get()));
|
|
|
|
inPipeTheirSide.reset();
|
|
outPipeTheirSide.reset();
|
|
signalPipeTheirSide.reset();
|
|
|
|
const auto commandLine = fmt::format(FMT_COMPILE(L" --headless --signal {:#x}"), (int64_t)signalPipeOurSide.release());
|
|
|
|
ConsoleArguments consoleArgs(commandLine, inPipeOurSide.release(), outPipeOurSide.release());
|
|
RETURN_IF_FAILED(consoleArgs.ParseCommandline());
|
|
|
|
return ConsoleCreateIoThread(Server, &consoleArgs, driverInputEvent, connectMessage);
|
|
#endif // TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
// Routine Description:
|
|
// - Creates the I/O thread for handling and processing messages from the console driver
|
|
// as the server side of a console session.
|
|
// - This entrypoint is for all start scenarios that are not receiving a hand-off
|
|
// from another console server. For example, getting started by kernelbase.dll from
|
|
// the operating system as a client application realizes it needs a console server,
|
|
// getting started to be a ConPTY host inside the OS, or being double clicked either
|
|
// inside the OS as `conhost.exe` or outside as `OpenConsole.exe`.
|
|
// Arguments:
|
|
// - Server - The server side handle to the console driver to let us pick up messages to process for the clients.
|
|
// - args - A structure of arguments that may have been passed in on the command-line, typically only used to control the ConPTY configuration.
|
|
// Return Value:
|
|
// - S_OK if the thread starts up correctly or any number of thread, registry, windowing, or just about any other
|
|
// failure that could possibly occur during console server initialization.
|
|
[[nodiscard]] HRESULT ConsoleCreateIoThreadLegacy(_In_ HANDLE Server, const ConsoleArguments* const args)
|
|
{
|
|
return ConsoleCreateIoThread(Server, args, INVALID_HANDLE_VALUE, nullptr);
|
|
}
|
|
|
|
#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 connection contains information not sent by clients in their connect IOs and initialized 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]] bool ConsoleConnectionDeservesVisibleWindow(PCONSOLE_API_CONNECTINFO p)
|
|
{
|
|
Globals& g = ServiceLocator::LocateGlobals();
|
|
// processes that are created ...
|
|
// ... with CREATE_NO_WINDOW never get a window.
|
|
// ... on Desktop, with a visible window always get one (even a fake one)
|
|
// ... not on Desktop, with a visible window only get one if we are headful (not ConPTY).
|
|
// This prevents pseudoconsole-hosted applications from taking over the screen,
|
|
// even if they really beg us for a window.
|
|
return p->WindowVisible && (s_IsOnDesktop() || !g.IsHeadless());
|
|
}
|
|
|
|
[[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) && ConsoleConnectionDeservesVisibleWindow(p))
|
|
{
|
|
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 lock the console so we must first unlock it ourselves.
|
|
UnlockConsole();
|
|
g.hConsoleInputInitEvent.wait();
|
|
LockConsole();
|
|
|
|
// 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.
|
|
|
|
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:
|
|
// - lpParameter - PCONSOLE_API_MSG being handed off to us from the previous I/O.
|
|
// 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;
|
|
|
|
// If we were given a message on startup, process that in our context and then continue with the IO loop normally.
|
|
if (lpParameter)
|
|
{
|
|
// Capture the incoming lpParameter into a unique_ptr so we can appropriately
|
|
// free the heap memory when we're done getting the important bits out of it below.
|
|
std::unique_ptr<CONSOLE_API_MSG> capturedMessage{ static_cast<PCONSOLE_API_MSG>(lpParameter) };
|
|
|
|
ReceiveMsg = *capturedMessage.get();
|
|
ReceiveMsg._pApiRoutines = &globals.api;
|
|
ReceiveMsg._pDeviceComm = globals.pDeviceComm;
|
|
IoSorter::ServiceIoOperation(&ReceiveMsg, &ReplyMsg);
|
|
}
|
|
|
|
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))
|
|
{
|
|
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;
|
|
}
|