Merge branch 'dev/migrie/f/non-terminal-content-elevation-warning' into dev/migrie/f/632-on-warning-dialog
This commit is contained in:
commit
1b4efadbd8
1
.github/actions/spelling/allow/allow.txt
vendored
1
.github/actions/spelling/allow/allow.txt
vendored
|
@ -42,6 +42,7 @@ Lmid
|
|||
Lorigin
|
||||
maxed
|
||||
mkmk
|
||||
mnt
|
||||
mru
|
||||
noreply
|
||||
nje
|
||||
|
|
2
.github/actions/spelling/allow/apis.txt
vendored
2
.github/actions/spelling/allow/apis.txt
vendored
|
@ -19,6 +19,7 @@ COLORPROPERTY
|
|||
colspan
|
||||
COMDLG
|
||||
comparand
|
||||
commandlinetoargv
|
||||
cstdint
|
||||
CXICON
|
||||
CYICON
|
||||
|
@ -168,6 +169,7 @@ toupper
|
|||
TTask
|
||||
TVal
|
||||
UChar
|
||||
ULARGE
|
||||
UPDATEINIFILE
|
||||
userenv
|
||||
wcsstr
|
||||
|
|
|
@ -27,6 +27,7 @@ namespace Microsoft.Terminal.Control
|
|||
interface IControlSettings requires Microsoft.Terminal.Core.ICoreSettings, Microsoft.Terminal.Control.IControlAppearance
|
||||
{
|
||||
String ProfileName;
|
||||
String ProfileSource;
|
||||
|
||||
Boolean UseAcrylic;
|
||||
ScrollbarState ScrollState;
|
||||
|
|
|
@ -2271,6 +2271,42 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
|||
}
|
||||
|
||||
std::wstring fullPath{ item.Path() };
|
||||
|
||||
// Fix path for WSL
|
||||
if (_settings.ProfileSource() == L"Windows.Terminal.Wsl")
|
||||
{
|
||||
std::replace(fullPath.begin(), fullPath.end(), L'\\', L'/');
|
||||
|
||||
if (fullPath.size() >= 2 && fullPath.at(1) == L':')
|
||||
{
|
||||
// C:/foo/bar -> Cc/foo/bar
|
||||
fullPath.at(1) = til::tolower_ascii(fullPath.at(0));
|
||||
// Cc/foo/bar -> /mnt/c/foo/bar
|
||||
fullPath.replace(0, 1, L"/mnt/");
|
||||
}
|
||||
else
|
||||
{
|
||||
static constexpr std::wstring_view wslPathPrefixes[] = { L"//wsl.localhost/", L"//wsl$/" };
|
||||
for (auto prefix : wslPathPrefixes)
|
||||
{
|
||||
if (til::starts_with(fullPath, prefix))
|
||||
{
|
||||
if (const auto idx = fullPath.find(L'/', prefix.size()); idx != std::wstring::npos)
|
||||
{
|
||||
// //wsl.localhost/Ubuntu-18.04/foo/bar -> /foo/bar
|
||||
fullPath.erase(0, idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// //wsl.localhost/Ubuntu-18.04 -> /
|
||||
fullPath = L"/";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto containsSpaces = std::find(fullPath.begin(),
|
||||
fullPath.end(),
|
||||
L' ') != fullPath.end();
|
||||
|
@ -2283,6 +2319,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
|||
|
||||
allPaths += fullPath;
|
||||
}
|
||||
|
||||
_core.PasteText(winrt::hstring{ allPaths });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,6 +145,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
try
|
||||
{
|
||||
LOG_LAST_ERROR_IF(!DeleteFile(_sharedPath.c_str()));
|
||||
LOG_LAST_ERROR_IF(!DeleteFile(_elevatedPath.c_str()));
|
||||
*_state.lock() = {};
|
||||
}
|
||||
CATCH_LOG()
|
||||
|
|
|
@ -86,7 +86,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
};
|
||||
til::shared_mutex<state_t> _state;
|
||||
std::filesystem::path _sharedPath;
|
||||
std::filesystem::path _userPath;
|
||||
std::filesystem::path _elevatedPath;
|
||||
til::throttled_func_trailing<> _throttler;
|
||||
|
||||
|
|
|
@ -49,10 +49,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
|||
// by the Builtin\Administrators group. If it's not, then it was likely
|
||||
// tampered with.
|
||||
// Arguments:
|
||||
// - path: the path to the file to check
|
||||
// - handle: a HANDLE to the file to check
|
||||
// Return Value:
|
||||
// - true if it had the expected permissions. False otherwise.
|
||||
static bool _hasElevatedOnlyPermissions(const std::filesystem::path& path)
|
||||
static bool _isOwnedByAdministrators(const HANDLE& handle)
|
||||
{
|
||||
// If the file is owned by the administrators group, trust the
|
||||
// administrators instead of checking the DACL permissions. It's simpler
|
||||
|
@ -62,14 +62,14 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
|||
PSID psidOwner{ nullptr };
|
||||
// The psidOwner pointer references the security descriptor, so it
|
||||
// doesn't have to be freed separate from sd.
|
||||
const auto status = GetNamedSecurityInfoW(path.c_str(),
|
||||
SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION,
|
||||
&psidOwner,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
wil::out_param_ptr<PSECURITY_DESCRIPTOR*>(sd));
|
||||
const auto status = GetSecurityInfo(handle,
|
||||
SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION,
|
||||
&psidOwner,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
wil::out_param_ptr<PSECURITY_DESCRIPTOR*>(sd));
|
||||
THROW_IF_WIN32_ERROR(status);
|
||||
|
||||
wil::unique_any_psid psidAdmins{ nullptr };
|
||||
|
@ -82,27 +82,46 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
|||
// Strips the UTF8 BOM if it exists.
|
||||
std::string ReadUTF8File(const std::filesystem::path& path, const bool elevatedOnly)
|
||||
{
|
||||
if (elevatedOnly)
|
||||
{
|
||||
const bool hadExpectedPermissions{ _hasElevatedOnlyPermissions(path) };
|
||||
if (!hadExpectedPermissions)
|
||||
{
|
||||
// delete the file. It's been compromised.
|
||||
LOG_LAST_ERROR_IF(!DeleteFile(path.c_str()));
|
||||
// Exit early, because obviously there's nothing to read from the deleted file.
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// From some casual observations we can determine that:
|
||||
// * ReadFile() always returns the requested amount of data (unless the file is smaller)
|
||||
// * It's unlikely that the file was changed between GetFileSize() and ReadFile()
|
||||
// -> Lets add a retry-loop just in case, to not fail if the file size changed while reading.
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) };
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr) };
|
||||
THROW_LAST_ERROR_IF(!file);
|
||||
|
||||
// Open the file _first_, then check if it has the right
|
||||
// permissions. This prevents a "Time-of-check to time-of-use"
|
||||
// vulnerability where a malicious exe could delete the file and
|
||||
// replace it between us checking the permissions, and reading the
|
||||
// contents. We've got a handle to the file now, which means we're
|
||||
// going to read the contents of that instance of the file
|
||||
// regardless. If someone replaces the file on us before we get to
|
||||
// the GetSecurityInfo call below, then only the subsequent call to
|
||||
// ReadUTF8File will notice it.
|
||||
if (elevatedOnly)
|
||||
{
|
||||
const bool hadExpectedPermissions{ _isOwnedByAdministrators(file.get()) };
|
||||
if (!hadExpectedPermissions)
|
||||
{
|
||||
// Close the handle
|
||||
file.reset();
|
||||
|
||||
// delete the file. It's been compromised.
|
||||
LOG_LAST_ERROR_IF(!DeleteFile(path.c_str()));
|
||||
|
||||
// Exit early, because obviously there's nothing to read from the deleted file.
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const auto fileSize = GetFileSize(file.get(), nullptr);
|
||||
THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE);
|
||||
|
||||
|
@ -204,7 +223,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
|||
// If we're running in an elevated context, when this file is
|
||||
// created, it will automatically be owned by
|
||||
// Builtin\Administrators, which will pass the above
|
||||
// _hasElevatedOnlyPermissions check.
|
||||
// _isOwnedByAdministrators check.
|
||||
//
|
||||
// Programs running in an elevated context will be free to write the
|
||||
// file, and unelevated processes will be able to read the file. An
|
||||
|
@ -216,7 +235,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model
|
|||
|
||||
wil::unique_hfile file{ CreateFileW(path.c_str(),
|
||||
GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_DELETE,
|
||||
elevatedOnly ? &sa : nullptr,
|
||||
CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
|
|
|
@ -283,6 +283,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
|
||||
// Fill in the remaining properties from the profile
|
||||
_ProfileName = profile.Name();
|
||||
_ProfileSource = profile.Source();
|
||||
_UseAcrylic = profile.UseAcrylic();
|
||||
|
||||
_FontFace = profile.FontInfo().FontFace();
|
||||
|
|
|
@ -119,6 +119,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
|
|||
// ------------------------ End of Core Settings -----------------------
|
||||
|
||||
INHERITABLE_SETTING(Model::TerminalSettings, hstring, ProfileName);
|
||||
INHERITABLE_SETTING(Model::TerminalSettings, hstring, ProfileSource);
|
||||
|
||||
INHERITABLE_SETTING(Model::TerminalSettings, bool, UseAcrylic, false);
|
||||
INHERITABLE_SETTING(Model::TerminalSettings, double, Opacity, UseAcrylic() ? 0.5 : 1.0);
|
||||
INHERITABLE_SETTING(Model::TerminalSettings, hstring, Padding, DEFAULT_PADDING);
|
||||
|
|
|
@ -51,6 +51,7 @@ namespace ControlUnitTests
|
|||
// ------------------------ End of Core Settings -----------------------
|
||||
|
||||
WINRT_PROPERTY(winrt::hstring, ProfileName);
|
||||
WINRT_PROPERTY(winrt::hstring, ProfileSource);
|
||||
WINRT_PROPERTY(bool, UseAcrylic, false);
|
||||
WINRT_PROPERTY(double, Opacity, .5);
|
||||
WINRT_PROPERTY(winrt::hstring, Padding, DEFAULT_PADDING);
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
// GetConsoleAlias
|
||||
using namespace WEX::TestExecution;
|
||||
using namespace WEX::Common;
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
|
||||
class AliasTests
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
// This class is intended to test boundary conditions for:
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
#include "precomp.h"
|
||||
|
||||
using namespace WEX::TestExecution;
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
// This class is intended to test:
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
#include <future>
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using WEX::TestExecution::TestData;
|
||||
using namespace WEX::Common;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
using namespace WEX::TestExecution;
|
||||
using namespace WEX::Common;
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
|
||||
static const COORD c_coordZero = { 0, 0 };
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
#define NUMBER_OF_SCENARIO_INPUTS 10
|
||||
#define READ_BATCH 3
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
// This class is intended to test:
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
using namespace Microsoft::Console::Types;
|
||||
using namespace WEX::TestExecution;
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
class OutputTests
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "precomp.h"
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
|
||||
// This class is intended to test restrictions placed on APIs from within a UWP application context
|
||||
class PolicyTests
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "precomp.h"
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
// This class is intended to test:
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
#define ENGLISH_US_CP 437u
|
||||
#define JAPANESE_CP 932u
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using WEX::TestExecution::TestData;
|
||||
using namespace WEX::Common;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "precomp.h"
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
// This class is intended to provide a canary (simple launch test)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "precomp.h"
|
||||
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
using namespace WEX::Common;
|
||||
|
||||
HANDLE Common::_hConsole = INVALID_HANDLE_VALUE;
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
using namespace WEX::TestExecution;
|
||||
using namespace WEX::Common;
|
||||
using WEX::Logging::Log;
|
||||
using namespace WEX::Logging;
|
||||
|
||||
class KeyPressTests
|
||||
{
|
||||
|
|
|
@ -34,13 +34,13 @@ using Microsoft::Console::Interactivity::ServiceLocator;
|
|||
// - OriginalCursorPosition -
|
||||
// - NumberOfVisibleChars
|
||||
// - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application
|
||||
// - CommandHistory -
|
||||
// - Echo -
|
||||
// - InsertMode -
|
||||
// - Processed -
|
||||
// - Line -
|
||||
// - pTempHandle - A handle to the output buffer to prevent it from being destroyed while we're using it to present 'edit line' text.
|
||||
// - initialData - any text data that should be prepopulated into the buffer
|
||||
// - pClientProcess - Attached process handle object
|
||||
// Return Value:
|
||||
// - THROW: Throws E_INVALIDARG for invalid pointers.
|
||||
COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
|
||||
|
@ -49,9 +49,9 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
|
|||
_In_ size_t UserBufferSize,
|
||||
_In_ PWCHAR UserBuffer,
|
||||
_In_ ULONG CtrlWakeupMask,
|
||||
_In_ CommandHistory* CommandHistory,
|
||||
const std::wstring_view exeName,
|
||||
const std::string_view initialData) :
|
||||
_In_ const std::wstring_view exeName,
|
||||
_In_ const std::string_view initialData,
|
||||
_In_ ConsoleProcessHandle* const pClientProcess) :
|
||||
ReadData(pInputBuffer, pInputReadHandleData),
|
||||
_screenInfo{ screenInfo },
|
||||
_bytesRead{ 0 },
|
||||
|
@ -62,7 +62,7 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
|
|||
_exeName{ exeName },
|
||||
_pdwNumBytes{ nullptr },
|
||||
|
||||
_commandHistory{ CommandHistory },
|
||||
_commandHistory{ CommandHistory::s_Find((HANDLE)pClientProcess) },
|
||||
_controlKeyState{ 0 },
|
||||
_ctrlWakeupMask{ CtrlWakeupMask },
|
||||
_visibleCharCount{ 0 },
|
||||
|
@ -73,7 +73,8 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
|
|||
_lineInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_LINE_INPUT) },
|
||||
_processedInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) },
|
||||
_insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() },
|
||||
_unicode{ false }
|
||||
_unicode{ false },
|
||||
_clientProcess{ pClientProcess }
|
||||
{
|
||||
#ifndef UNIT_TESTING
|
||||
THROW_IF_FAILED(screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output,
|
||||
|
@ -1008,13 +1009,13 @@ void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline
|
|||
{
|
||||
// Figure out where real string ends (at carriage return or end of buffer).
|
||||
PWCHAR StringPtr = _backupLimit;
|
||||
size_t StringLength = _bytesRead;
|
||||
size_t StringLength = _bytesRead / sizeof(WCHAR);
|
||||
bool FoundCR = false;
|
||||
for (size_t i = 0; i < (_bytesRead / sizeof(WCHAR)); i++)
|
||||
for (size_t i = 0; i < StringLength; i++)
|
||||
{
|
||||
if (*StringPtr++ == UNICODE_CARRIAGERETURN)
|
||||
{
|
||||
StringLength = i * sizeof(WCHAR);
|
||||
StringLength = i;
|
||||
FoundCR = true;
|
||||
break;
|
||||
}
|
||||
|
@ -1026,11 +1027,13 @@ void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline
|
|||
{
|
||||
// add to command line recall list if we have a history list.
|
||||
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
||||
LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, StringLength / sizeof(wchar_t) },
|
||||
LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, StringLength },
|
||||
WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
|
||||
}
|
||||
|
||||
Tracing::s_TraceCookedRead(_backupLimit);
|
||||
Tracing::s_TraceCookedRead(_clientProcess,
|
||||
_backupLimit,
|
||||
base::saturated_cast<ULONG>(StringLength));
|
||||
|
||||
// check for alias
|
||||
ProcessAliases(LineCount);
|
||||
|
|
|
@ -39,9 +39,9 @@ public:
|
|||
_In_ size_t UserBufferSize,
|
||||
_In_ PWCHAR UserBuffer,
|
||||
_In_ ULONG CtrlWakeupMask,
|
||||
_In_ CommandHistory* CommandHistory,
|
||||
const std::wstring_view exeName,
|
||||
const std::string_view initialData);
|
||||
_In_ const std::wstring_view exeName,
|
||||
_In_ const std::string_view initialData,
|
||||
_In_ ConsoleProcessHandle* const pClientProcess);
|
||||
|
||||
~COOKED_READ_DATA() override;
|
||||
COOKED_READ_DATA(COOKED_READ_DATA&&) = default;
|
||||
|
@ -156,6 +156,8 @@ private:
|
|||
bool _insertMode;
|
||||
bool _unicode;
|
||||
|
||||
ConsoleProcessHandle* const _clientProcess;
|
||||
|
||||
[[nodiscard]] NTSTATUS _readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept;
|
||||
|
||||
[[nodiscard]] NTSTATUS _handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept;
|
||||
|
|
|
@ -133,6 +133,7 @@ CRTLIBS = \
|
|||
|
||||
TARGETLIBS = \
|
||||
$(TARGETLIBS) \
|
||||
$(MINCORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\api-ms-win-core-commandlinetoargv-l1.lib \
|
||||
$(ONECORE_INTERNAL_SDK_LIB_PATH)\onecoreuuid.lib \
|
||||
$(ONECOREUAP_INTERNAL_SDK_LIB_PATH)\onecoreuapuuid.lib \
|
||||
$(ONECORE_INTERNAL_PRIV_SDK_LIB_VPATH_L)\onecore_internal.lib \
|
||||
|
|
|
@ -471,7 +471,6 @@ size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX,
|
|||
RETURN_HR_IF(E_FAIL, !gci.HasActiveOutputBuffer());
|
||||
|
||||
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
|
||||
CommandHistory* const pCommandHistory = CommandHistory::s_Find(processData);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -481,9 +480,9 @@ size_t RetrieveNumberOfSpaces(_In_ SHORT sOriginalCursorPositionX,
|
|||
buffer.size_bytes(), // UserBufferSize
|
||||
reinterpret_cast<wchar_t*>(buffer.data()), // UserBuffer
|
||||
ctrlWakeupMask, // CtrlWakeupMask
|
||||
pCommandHistory, // CommandHistory
|
||||
exeName, // exe name
|
||||
initialData);
|
||||
initialData,
|
||||
reinterpret_cast<ConsoleProcessHandle*>(processData)); //pClientProcess
|
||||
|
||||
gci.SetCookedReadData(cookedReadData.get());
|
||||
bytesRead = buffer.size_bytes(); // This parameter on the way in is the size to read, on the way out, it will be updated to what is actually read.
|
||||
|
|
|
@ -587,3 +587,8 @@ void Telemetry::LogRipMessage(_In_z_ const char* pszMessage, ...) const
|
|||
TraceLoggingString(szMessageEvaluated, "Message"));
|
||||
}
|
||||
}
|
||||
|
||||
bool Telemetry::IsUserInteractive()
|
||||
{
|
||||
return _fUserInteractiveForTelemetry;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ public:
|
|||
|
||||
void LogRipMessage(_In_z_ const char* pszMessage, ...) const;
|
||||
|
||||
bool IsUserInteractive();
|
||||
|
||||
// Names are from the external API call names. Note that some names can be different
|
||||
// than the internal API calls.
|
||||
// Don't worry about the following APIs, because they are external to our conhost codebase and hard to track through
|
||||
|
|
|
@ -21,6 +21,7 @@ enum TraceKeywords
|
|||
API = 0x400,
|
||||
UIA = 0x800,
|
||||
CookedRead = 0x1000,
|
||||
ConsoleAttachDetach = 0x2000,
|
||||
All = 0x1FFF
|
||||
};
|
||||
DEFINE_ENUM_FLAG_OPERATORS(TraceKeywords);
|
||||
|
@ -405,14 +406,38 @@ void Tracing::s_TraceInputRecord(const INPUT_RECORD& inputRecord)
|
|||
}
|
||||
}
|
||||
|
||||
void Tracing::s_TraceCookedRead(_In_z_ const wchar_t* pwszCookedBuffer)
|
||||
void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength)
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"CookedRead",
|
||||
TraceLoggingWideString(pwszCookedBuffer, "ReadBuffer"),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::CookedRead));
|
||||
if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::CookedRead))
|
||||
{
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"CookedRead",
|
||||
TraceLoggingCountedWideString(pwchCookedBuffer, cchCookedBufferLength, "ReadBuffer"),
|
||||
TraceLoggingULong(cchCookedBufferLength, "ReadBufferLength"),
|
||||
TraceLoggingUInt32(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"),
|
||||
TraceLoggingUInt64(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::CookedRead));
|
||||
}
|
||||
}
|
||||
|
||||
void Tracing::s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_ bool bIsAttach)
|
||||
{
|
||||
if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::ConsoleAttachDetach))
|
||||
{
|
||||
bool bIsUserInteractive = Telemetry::Instance().IsUserInteractive();
|
||||
|
||||
TraceLoggingWrite(
|
||||
g_hConhostV2EventTraceProvider,
|
||||
"ConsoleAttachDetach",
|
||||
TraceLoggingUInt32(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"),
|
||||
TraceLoggingUInt64(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"),
|
||||
TraceLoggingBool(bIsAttach, "IsAttach"),
|
||||
TraceLoggingBool(bIsUserInteractive, "IsUserInteractive"),
|
||||
TraceLoggingKeyword(TIL_KEYWORD_TRACE),
|
||||
TraceLoggingKeyword(TraceKeywords::ConsoleAttachDetach));
|
||||
}
|
||||
}
|
||||
|
||||
void __stdcall Tracing::TraceFailure(const wil::FailureInfo& failure) noexcept
|
||||
|
|
|
@ -62,7 +62,8 @@ public:
|
|||
static void s_TraceWindowMessage(const MSG& msg);
|
||||
static void s_TraceInputRecord(const INPUT_RECORD& inputRecord);
|
||||
|
||||
static void s_TraceCookedRead(_In_z_ const wchar_t* pwszCookedBuffer);
|
||||
static void s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength);
|
||||
static void s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_ bool bIsAttach);
|
||||
|
||||
static void __stdcall TraceFailure(const wil::FailureInfo& failure) noexcept;
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ class ApiRoutinesTests
|
|||
|
||||
ApiRoutines _Routines;
|
||||
IApiRoutines* _pApiRoutines = &_Routines;
|
||||
CommandHistory* m_pHistory;
|
||||
|
||||
TEST_METHOD_SETUP(MethodSetup)
|
||||
{
|
||||
|
@ -37,11 +38,20 @@ class ApiRoutinesTests
|
|||
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
if (!m_pHistory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_CLEANUP(MethodCleanup)
|
||||
{
|
||||
CommandHistory::s_Free(nullptr);
|
||||
m_pHistory = nullptr;
|
||||
|
||||
m_state->CleanupGlobalInputBuffer();
|
||||
|
||||
m_state->CleanupGlobalScreenBuffer();
|
||||
|
|
|
@ -43,12 +43,13 @@ class CommandLineTests
|
|||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
m_state->PrepareReadHandle();
|
||||
m_state->PrepareCookedReadData();
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
if (!m_pHistory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ (as it uses s_Find to get at it)
|
||||
m_state->PrepareCookedReadData();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,6 @@ class CommandListPopupTests
|
|||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
m_state->PrepareReadHandle();
|
||||
m_state->PrepareCookedReadData();
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
// resize command history storage to 50 items so that we don't cycle on accident
|
||||
// when PopupTestHelper::InitLongHistory() is called.
|
||||
|
@ -58,6 +57,8 @@ class CommandListPopupTests
|
|||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ (as it uses s_Find to get at it)
|
||||
m_state->PrepareCookedReadData();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,12 +45,13 @@ class CommandNumberPopupTests
|
|||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
m_state->PrepareReadHandle();
|
||||
m_state->PrepareCookedReadData();
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
if (!m_pHistory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ (as it uses s_Find to get at it)
|
||||
m_state->PrepareCookedReadData();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ class CopyFromCharPopupTests
|
|||
TEST_CLASS(CopyFromCharPopupTests);
|
||||
|
||||
std::unique_ptr<CommonState> m_state;
|
||||
CommandHistory* m_pHistory;
|
||||
|
||||
TEST_CLASS_SETUP(ClassSetup)
|
||||
{
|
||||
|
@ -43,12 +44,20 @@ class CopyFromCharPopupTests
|
|||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
m_state->PrepareReadHandle();
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
if (!m_pHistory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ (as it uses s_Find to get at it)
|
||||
m_state->PrepareCookedReadData();
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_METHOD_CLEANUP(MethodCleanup)
|
||||
{
|
||||
CommandHistory::s_Free(nullptr);
|
||||
m_pHistory = nullptr;
|
||||
m_state->CleanupCookedReadData();
|
||||
m_state->CleanupReadHandle();
|
||||
m_state->CleanupGlobalInputBuffer();
|
||||
|
|
|
@ -44,12 +44,13 @@ class CopyToCharPopupTests
|
|||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
m_state->PrepareReadHandle();
|
||||
m_state->PrepareCookedReadData();
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
if (!m_pHistory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ (as it uses s_Find to get at it)
|
||||
m_state->PrepareCookedReadData();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -412,6 +412,7 @@ class SelectionInputTests
|
|||
TEST_CLASS(SelectionInputTests);
|
||||
|
||||
CommonState* m_state;
|
||||
CommandHistory* m_pHistory;
|
||||
|
||||
TEST_CLASS_SETUP(ClassSetup)
|
||||
{
|
||||
|
@ -420,12 +421,20 @@ class SelectionInputTests
|
|||
m_state->PrepareGlobalFont();
|
||||
m_state->PrepareGlobalScreenBuffer();
|
||||
m_state->PrepareGlobalInputBuffer();
|
||||
m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr);
|
||||
if (!m_pHistory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// History must be prepared before COOKED_READ (as it uses s_Find to get at it)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_CLASS_CLEANUP(ClassCleanup)
|
||||
{
|
||||
CommandHistory::s_Free(nullptr);
|
||||
m_pHistory = nullptr;
|
||||
m_state->CleanupGlobalScreenBuffer();
|
||||
m_state->CleanupGlobalFont();
|
||||
m_state->CleanupGlobalInputBuffer();
|
||||
|
|
|
@ -132,9 +132,9 @@ public:
|
|||
0,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
L"",
|
||||
initialData);
|
||||
initialData,
|
||||
nullptr);
|
||||
gci.SetCookedReadData(readData);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ INCLUDES= \
|
|||
$(CONSOLE_SRC_PATH)\..\oss\fmt\include; \
|
||||
$(CONSOLE_SRC_PATH)\..\oss\interval_tree; \
|
||||
$(CONSOLE_SRC_PATH)\..\oss\boost\boost_1_73_0; \
|
||||
$(CONSOLE_SRC_PATH)\..\oss\pcg\include; \
|
||||
$(MINWIN_INTERNAL_PRIV_SDK_INC_PATH_L); \
|
||||
$(MINWIN_RESTRICTED_PRIV_SDK_INC_PATH_L); \
|
||||
$(MINCORE_INTERNAL_PRIV_SDK_INC_PATH_L); \
|
||||
|
|
|
@ -312,7 +312,7 @@
|
|||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
// ReadConsole needs this to get the command history list associated with an attached process, but it can be an opaque value.
|
||||
// ReadConsole needs this to get details associated with an attached process (such as the command history list, telemetry metadata).
|
||||
HANDLE const hConsoleClient = (HANDLE)m->GetProcessHandle();
|
||||
|
||||
// ReadConsole needs this to store context information across "processed reads" e.g. reads on the same handle
|
||||
|
|
|
@ -432,6 +432,8 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleHandleConnectionRequest(_In_ PCONSOLE_API
|
|||
gci.ProcessHandleList.FreeProcessData(ProcessData);
|
||||
}
|
||||
|
||||
Tracing::s_TraceConsoleAttachDetach(ProcessData, true);
|
||||
|
||||
UnlockConsole();
|
||||
|
||||
return nullptr;
|
||||
|
@ -470,6 +472,8 @@ PCONSOLE_API_MSG IoDispatchers::ConsoleClientDisconnectRoutine(_In_ PCONSOLE_API
|
|||
pNotifier->NotifyConsoleEndApplicationEvent(pProcessData->dwProcessId);
|
||||
}
|
||||
|
||||
Tracing::s_TraceConsoleAttachDetach(pProcessData, false);
|
||||
|
||||
LOG_IF_FAILED(RemoveConsole(pProcessData));
|
||||
|
||||
pMessage->SetReplyStatus(STATUS_SUCCESS);
|
||||
|
|
|
@ -27,7 +27,8 @@ ConsoleProcessHandle::ConsoleProcessHandle(const DWORD dwProcessId,
|
|||
FALSE,
|
||||
dwProcessId))),
|
||||
_policy(ConsoleProcessPolicy::s_CreateInstance(_hProcess.get())),
|
||||
_shimPolicy(ConsoleShimPolicy::s_CreateInstance(_hProcess.get()))
|
||||
_shimPolicy(ConsoleShimPolicy::s_CreateInstance(_hProcess.get())),
|
||||
_processCreationTime(0)
|
||||
{
|
||||
if (nullptr != _hProcess.get())
|
||||
{
|
||||
|
@ -65,3 +66,36 @@ const ConsoleShimPolicy ConsoleProcessHandle::GetShimPolicy() const
|
|||
{
|
||||
return _shimPolicy;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Retrieves the raw process handle
|
||||
const HANDLE ConsoleProcessHandle::GetRawHandle() const
|
||||
{
|
||||
return _hProcess.get();
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Retrieves the process creation time (currently used in telemetry traces)
|
||||
// - The creation time is lazily populated on first call
|
||||
const ULONG64 ConsoleProcessHandle::GetProcessCreationTime() const
|
||||
{
|
||||
if (_processCreationTime == 0 && _hProcess != nullptr)
|
||||
{
|
||||
FILETIME ftCreationTime, ftDummyTime = { 0 };
|
||||
ULARGE_INTEGER creationTime = { 0 };
|
||||
|
||||
if (::GetProcessTimes(_hProcess.get(),
|
||||
&ftCreationTime,
|
||||
&ftDummyTime,
|
||||
&ftDummyTime,
|
||||
&ftDummyTime))
|
||||
{
|
||||
creationTime.HighPart = ftCreationTime.dwHighDateTime;
|
||||
creationTime.LowPart = ftCreationTime.dwLowDateTime;
|
||||
}
|
||||
|
||||
_processCreationTime = creationTime.QuadPart;
|
||||
}
|
||||
|
||||
return _processCreationTime;
|
||||
}
|
||||
|
|
|
@ -40,8 +40,12 @@ public:
|
|||
const ConsoleProcessPolicy GetPolicy() const;
|
||||
const ConsoleShimPolicy GetShimPolicy() const;
|
||||
|
||||
const HANDLE GetRawHandle() const;
|
||||
|
||||
CD_CONNECTION_INFORMATION GetConnectionInformation(IDeviceComm* deviceComm) const;
|
||||
|
||||
const ULONG64 GetProcessCreationTime() const;
|
||||
|
||||
private:
|
||||
ConsoleProcessHandle(const DWORD dwProcessId,
|
||||
const DWORD dwThreadId,
|
||||
|
@ -56,6 +60,8 @@ private:
|
|||
ULONG const _ulProcessGroupId;
|
||||
wil::unique_handle const _hProcess;
|
||||
|
||||
mutable ULONG64 _processCreationTime;
|
||||
|
||||
const ConsoleProcessPolicy _policy;
|
||||
const ConsoleShimPolicy _shimPolicy;
|
||||
|
||||
|
|
|
@ -2509,53 +2509,55 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
|
|||
// - None
|
||||
void AdaptDispatch::_ReportSGRSetting() const
|
||||
{
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
// A valid response always starts with DCS 1 $ r.
|
||||
// Then the '0' parameter is to reset the SGR attributes to the defaults.
|
||||
std::wstring response = L"\033P1$r0";
|
||||
fmt::basic_memory_buffer<wchar_t, 64> response;
|
||||
response.append(L"\033P1$r0"sv);
|
||||
|
||||
TextAttribute attr;
|
||||
if (_pConApi->PrivateGetTextAttributes(attr))
|
||||
{
|
||||
// For each boolean attribute that is set, we add the appropriate
|
||||
// parameter value to the response string.
|
||||
const auto addAttribute = [&](const auto parameter, const auto enabled) {
|
||||
const auto addAttribute = [&](const auto& parameter, const auto enabled) {
|
||||
if (enabled)
|
||||
{
|
||||
response += parameter;
|
||||
response.append(parameter);
|
||||
}
|
||||
};
|
||||
addAttribute(L";1", attr.IsBold());
|
||||
addAttribute(L";2", attr.IsFaint());
|
||||
addAttribute(L";3", attr.IsItalic());
|
||||
addAttribute(L";4", attr.IsUnderlined());
|
||||
addAttribute(L";5", attr.IsBlinking());
|
||||
addAttribute(L";7", attr.IsReverseVideo());
|
||||
addAttribute(L";8", attr.IsInvisible());
|
||||
addAttribute(L";9", attr.IsCrossedOut());
|
||||
addAttribute(L";21", attr.IsDoublyUnderlined());
|
||||
addAttribute(L";53", attr.IsOverlined());
|
||||
addAttribute(L";1"sv, attr.IsBold());
|
||||
addAttribute(L";2"sv, attr.IsFaint());
|
||||
addAttribute(L";3"sv, attr.IsItalic());
|
||||
addAttribute(L";4"sv, attr.IsUnderlined());
|
||||
addAttribute(L";5"sv, attr.IsBlinking());
|
||||
addAttribute(L";7"sv, attr.IsReverseVideo());
|
||||
addAttribute(L";8"sv, attr.IsInvisible());
|
||||
addAttribute(L";9"sv, attr.IsCrossedOut());
|
||||
addAttribute(L";21"sv, attr.IsDoublyUnderlined());
|
||||
addAttribute(L";53"sv, attr.IsOverlined());
|
||||
|
||||
// We also need to add the appropriate color encoding parameters for
|
||||
// both the foreground and background colors.
|
||||
const auto addColor = [&](const auto base, const auto color) {
|
||||
const auto iterator = std::back_insert_iterator(response);
|
||||
if (color.IsIndex16())
|
||||
{
|
||||
const auto index = color.GetIndex();
|
||||
const auto colorParameter = base + (index >= 8 ? 60 : 0) + (index % 8);
|
||||
fmt::format_to(iterator, FMT_STRING(L";{}"), colorParameter);
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{}"), colorParameter);
|
||||
}
|
||||
else if (color.IsIndex256())
|
||||
{
|
||||
const auto index = color.GetIndex();
|
||||
fmt::format_to(iterator, FMT_STRING(L";{};5;{}"), base + 8, index);
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};5;{}"), base + 8, index);
|
||||
}
|
||||
else if (color.IsRgb())
|
||||
{
|
||||
const auto r = GetRValue(color.GetRGB());
|
||||
const auto g = GetGValue(color.GetRGB());
|
||||
const auto b = GetBValue(color.GetRGB());
|
||||
fmt::format_to(iterator, FMT_STRING(L";{};2;{};{};{}"), base + 8, r, g, b);
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L";{};2;{};{};{}"), base + 8, r, g, b);
|
||||
}
|
||||
};
|
||||
addColor(30, attr.GetForeground());
|
||||
|
@ -2563,8 +2565,8 @@ void AdaptDispatch::_ReportSGRSetting() const
|
|||
}
|
||||
|
||||
// The 'm' indicates this is an SGR response, and ST ends the sequence.
|
||||
response += L"m\033\\";
|
||||
_WriteResponse(response);
|
||||
response.append(L"m\033\\"sv);
|
||||
_WriteResponse({ response.data(), response.size() });
|
||||
}
|
||||
|
||||
// Method Description:
|
||||
|
@ -2575,8 +2577,11 @@ void AdaptDispatch::_ReportSGRSetting() const
|
|||
// - None
|
||||
void AdaptDispatch::_ReportDECSTBMSetting() const
|
||||
{
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
// A valid response always starts with DCS 1 $ r.
|
||||
std::wstring response = L"\033P1$r";
|
||||
fmt::basic_memory_buffer<wchar_t, 64> response;
|
||||
response.append(L"\033P1$r"sv);
|
||||
|
||||
CONSOLE_SCREEN_BUFFER_INFOEX csbiex = { 0 };
|
||||
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
|
||||
|
@ -2592,11 +2597,10 @@ void AdaptDispatch::_ReportDECSTBMSetting() const
|
|||
marginTop = 1;
|
||||
marginBottom = csbiex.srWindow.Bottom - csbiex.srWindow.Top;
|
||||
}
|
||||
const auto iterator = std::back_insert_iterator(response);
|
||||
fmt::format_to(iterator, FMT_STRING(L"{};{}"), marginTop, marginBottom);
|
||||
fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginTop, marginBottom);
|
||||
}
|
||||
|
||||
// The 'r' indicates this is an DECSTBM response, and ST ends the sequence.
|
||||
response += L"r\033\\";
|
||||
_WriteResponse(response);
|
||||
response.append(L"r\033\\"sv);
|
||||
_WriteResponse({ response.data(), response.size() });
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include "WexTestClass.h"
|
||||
|
||||
#include <til/rand.h>
|
||||
#include <wincrypt.h>
|
||||
|
||||
#include "base64.hpp"
|
||||
|
||||
|
|
Loading…
Reference in a new issue