commit 02d2f93807c9851d73eaacb4d8811a76b64b7b01 parent 978bc13b45142c7d96aa3576b4ae786fcb9208db Author: JohnMcPMS <johnmcp@microsoft.com> Date: Sat, 15 Apr 2023 14:02:56 -0700 Add configuration telemetry events (#3152) This change adds telemetry events from configuration actions, as well as more details to improve the error reporting (both in telemetry and to the user). The telemetry events can be disabled in code by the front end. For `winget configure`, the winget setting that controls telemetry is flowed through into the configuration code. Two telemetry events are added by this change: - An event with details on a failed attempt to execute a configuration unit. This will only be logged for failures of publicly available units. It contains the name of the unit and module, as well as the names of the top level settings provided (but *not* the values). - An event with a summary of processing a configuration set, containing the overall result and error attribution, as well as the counts of configuration units, runs, and failures. Additionally, another string was added to the result information. The intention is that the existing string (`Description`) should be used for a "short", user presentable message. The new string (`Details`), can contain a longer value that is intended for logs or a "more details" type experience. The PowerShell processor was changed to wrap almost all exceptions into the result information object now that the source can be clearly stated there. Failures coming directly from invoking the resource are attributed to the configuration unit processing, except in the cases where we detect the signature of an invalid setting value (then the configuration set [author] is blamed). All other exceptions are treated as an internal error. Diffstat:
68 files changed, 2110 insertions(+), 688 deletions(-)
diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -356,7 +356,8 @@ namespace AppInstaller::CLI::Workflow if (FAILED(resultInformation.ResultCode())) { AICLI_LOG(Config, Error, << "Failed to get unit details for " << Utility::ConvertToUTF8(unit.UnitName()) << " : 0x" << - Logging::SetHRFormat << resultInformation.ResultCode() << '\n' << Utility::ConvertToUTF8(resultInformation.Description())); + Logging::SetHRFormat << resultInformation.ResultCode() << '\n' << Utility::ConvertToUTF8(resultInformation.Description()) << '\n' << + Utility::ConvertToUTF8(resultInformation.Details())); } } @@ -435,7 +436,8 @@ namespace AppInstaller::CLI::Workflow else { AICLI_LOG(Config, Error, << "Configuration unit " << Utility::ConvertToUTF8(unit.UnitName()) << "[" << Utility::ConvertToUTF8(unit.Identifier()) << "] failed with code 0x" - << Logging::SetHRFormat << resultInformation.ResultCode() << " and error message:\n" << Utility::ConvertToUTF8(resultInformation.Description())); + << Logging::SetHRFormat << resultInformation.ResultCode() << " and error message:\n" << Utility::ConvertToUTF8(resultInformation.Description()) << '\n' + << Utility::ConvertToUTF8(resultInformation.Details())); // TODO: Improve error reporting for failures: use message, known HRs, getting HR system string, etc. m_context.Reporter.Error() << " "_liv << Resource::String::ConfigurationUnitFailed << " 0x"_liv << Logging::SetHRFormat << resultInformation.ResultCode() << std::endl; } @@ -485,6 +487,11 @@ namespace AppInstaller::CLI::Workflow // Set the processor to the current level of the logging. processor.MinimumLevel(ConvertLevel(Logging::Log().GetLevel())); + processor.Caller(L"winget"); + // Use same activity as the overall winget command + processor.ActivityIdentifier(*Logging::Telemetry().GetActivityId()); + // Apply winget telemetry setting to configuration + processor.GenerateTelemetryEvents(!Settings::User().Get<Settings::Setting::TelemetryDisable>()); // Route the configuration diagnostics into the context's diagnostics logging processor.Diagnostics([&context](const winrt::Windows::Foundation::IInspectable&, const DiagnosticInformation& diagnostics) diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -329,9 +329,7 @@ <ClInclude Include="Public\winget\TraceLogger.h" /> <ClInclude Include="Public\winget\UserSettings.h" /> <ClInclude Include="Public\winget\WindowsFeature.h" /> - <ClInclude Include="Telemetry\MicrosoftTelemetry.h" /> <ClInclude Include="Telemetry\TraceLogging.h" /> - <ClInclude Include="Telemetry\WinEventLogLevels.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="AdminSettings.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -13,9 +13,6 @@ <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier> <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions> </Filter> - <Filter Include="Telemetry - Do Not Modify"> - <UniqueIdentifier>{552a58eb-8d07-41b2-87b5-3e71b9fb3cfd}</UniqueIdentifier> - </Filter> <Filter Include="Public"> <UniqueIdentifier>{5cdf3fa3-e657-4d84-81bb-f740aa476143}</UniqueIdentifier> </Filter> @@ -28,19 +25,16 @@ <Filter Include="Manifest"> <UniqueIdentifier>{9b8e2682-3eb7-4530-bc9a-a57fafc44177}</UniqueIdentifier> </Filter> + <Filter Include="Telemetry"> + <UniqueIdentifier>{552a58eb-8d07-41b2-87b5-3e71b9fb3cfd}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> <Filter>Header Files</Filter> </ClInclude> - <ClInclude Include="Telemetry\MicrosoftTelemetry.h"> - <Filter>Telemetry - Do Not Modify</Filter> - </ClInclude> <ClInclude Include="Telemetry\TraceLogging.h"> - <Filter>Telemetry - Do Not Modify</Filter> - </ClInclude> - <ClInclude Include="Telemetry\WinEventLogLevels.h"> - <Filter>Telemetry - Do Not Modify</Filter> + <Filter>Telemetry</Filter> </ClInclude> <ClInclude Include="Public\AppInstallerTelemetry.h"> <Filter>Public</Filter> @@ -198,7 +192,7 @@ <Filter>Source Files</Filter> </ClCompile> <ClCompile Include="Telemetry\TraceLogging.cpp"> - <Filter>Telemetry - Do Not Modify</Filter> + <Filter>Telemetry</Filter> </ClCompile> <ClCompile Include="AppInstallerTelemetry.cpp"> <Filter>Source Files</Filter> diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -21,23 +21,6 @@ TraceLoggingCountedUtf8String(m_caller.c_str(), static_cast<ULONG>(m_caller.siz TraceLoggingPackedFieldEx(m_telemetryCorrelationJsonW.c_str(), static_cast<ULONG>((m_telemetryCorrelationJsonW.size() + 1) * sizeof(wchar_t)), TlgInUNICODESTRING, TlgOutJSON, "CvJson"),\ __VA_ARGS__) -// Helper to print a GUID -std::ostream& operator<<(std::ostream& out, const GUID& guid) -{ - wchar_t buffer[256]; - - if (StringFromGUID2(guid, buffer, ARRAYSIZE(buffer))) - { - out << AppInstaller::Utility::ConvertToUTF8(buffer); - } - else - { - out << "error"; - } - - return out; -} - namespace AppInstaller::Logging { using namespace Utility; @@ -856,7 +839,7 @@ namespace AppInstaller::Logging ThreadLocalStorage::ThreadGlobals* pThreadGlobals = ThreadLocalStorage::ThreadGlobals::GetForCurrentThread(); if (pThreadGlobals) { - return pThreadGlobals->GetTelemetryLogger(); + return *reinterpret_cast<TelemetryTraceLogger*>(pThreadGlobals->GetTelemetryObject()); } else { diff --git a/src/AppInstallerCommonCore/FileLogger.cpp b/src/AppInstallerCommonCore/FileLogger.cpp @@ -72,7 +72,7 @@ namespace AppInstaller::Logging // Just eat any exceptions here; better than losing logs } - void FileLogger::WriteDirect(std::string_view message) noexcept try + void FileLogger::WriteDirect(Channel, Level, std::string_view message) noexcept try { m_stream << message << std::endl; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerFileLogger.h b/src/AppInstallerCommonCore/Public/AppInstallerFileLogger.h @@ -35,7 +35,7 @@ namespace AppInstaller::Logging void Write(Channel channel, Level level, std::string_view message) noexcept override; - void WriteDirect(std::string_view message) noexcept override; + void WriteDirect(Channel channel, Level level, std::string_view message) noexcept override; // Adds a FileLogger to the current Log static void Add(); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -3,6 +3,7 @@ #pragma once #include <AppInstallerVersions.h> #include <winget/LocIndependent.h> +#include <winget/Runtime.h> #include <filesystem> #include <memory> @@ -11,22 +12,6 @@ namespace AppInstaller::Runtime { - // Determines whether the process is running in a packaged context or not. - bool IsRunningInPackagedContext(); - - // Determines the current version of the client and returns it. - Utility::LocIndString GetClientVersion(); - - // Determines the current version of the package if running in a packaged context. - Utility::LocIndString GetPackageVersion(); - - // Gets a string representation of the OS version for debugging purposes. - Utility::LocIndString GetOSVersion(); - - // Gets the OS region. - // This can be used as the current market. - std::string GetOSRegion(); - // Sets the runtime path state name globally. void SetRuntimePathStateName(std::string name); @@ -118,22 +103,9 @@ namespace AppInstaller::Runtime // Gets a new temp file path. std::filesystem::path GetNewTempFilePath(); - // Determines whether the current OS version is >= the given one. - // We treat the given Version struct as a standard 4 part Windows OS version. - bool IsCurrentOSVersionGreaterThanOrEqual(const Utility::Version& version); - - // Determines whether the process is running with administrator privileges. - bool IsRunningAsAdmin(); - - // Determines whether the process is running with local system context. - bool IsRunningAsSystem(); - // Determines whether developer mode is enabled. bool IsDevModeEnabled(); - // Returns true if this is a release build; false if not. - inline constexpr bool IsReleaseBuild(); - // Gets the default user agent string for the Windows Package Manager. Utility::LocIndString GetDefaultUserAgent(); diff --git a/src/AppInstallerCommonCore/Public/winget/ThreadGlobals.h b/src/AppInstallerCommonCore/Public/winget/ThreadGlobals.h @@ -19,7 +19,9 @@ namespace AppInstaller::ThreadLocalStorage AppInstaller::Logging::DiagnosticLogger& GetDiagnosticLogger() override; - AppInstaller::Logging::TelemetryTraceLogger& GetTelemetryLogger() override; + void* GetTelemetryObject() override; + + AppInstaller::Logging::TelemetryTraceLogger& GetTelemetryLogger(); // Set Globals for Current Thread // Return RAII object with it's ownership to set the AppInstaller ThreadLocalStorage back to previous state diff --git a/src/AppInstallerCommonCore/Public/winget/TraceLogger.h b/src/AppInstallerCommonCore/Public/winget/TraceLogger.h @@ -22,7 +22,7 @@ namespace AppInstaller::Logging void Write(Channel channel, Level, std::string_view message) noexcept override; - void WriteDirect(std::string_view message) noexcept override; + void WriteDirect(Channel channel, Level level, std::string_view message) noexcept override; // Adds a TraceLogger to the current Log static void Add(); diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp @@ -36,20 +36,11 @@ namespace AppInstaller::Runtime #ifndef WINGET_DISABLE_FOR_FUZZING constexpr std::string_view s_SecureSettings_Relative_Packaged = "pkg"sv; #endif - constexpr std::string_view s_PreviewBuildSuffix = "-preview"sv; constexpr std::string_view s_RuntimePath_Unpackaged_DefaultState = "defaultState"sv; static std::optional<std::string> s_runtimePathStateName; static wil::srwlock s_runtimePathStateNameLock; - // Gets a boolean indicating whether the current process has identity. - bool DoesCurrentProcessHaveIdentity() - { - UINT32 length = 0; - LONG result = GetPackageFamilyName(GetCurrentProcess(), &length, nullptr); - return (result != APPMODEL_ERROR_NO_PACKAGE); - } - // Gets the path to the root of the package containing the current process. std::filesystem::path GetPackagePath() { @@ -112,19 +103,6 @@ namespace AppInstaller::Runtime return Utility::ConvertToUTF8(packageId->name); } - // Gets the package version; only succeeds if running in a packaged context. - std::optional<PACKAGE_VERSION> GetPACKAGE_VERSION() - { - std::unique_ptr<byte[]> buffer = GetPACKAGE_ID(); - if (!buffer) - { - return {}; - } - - PACKAGE_ID* packageId = reinterpret_cast<PACKAGE_ID*>(buffer.get()); - return packageId->version; - } - #ifndef AICLI_DISABLE_TEST_HOOKS static std::map<PathName, PathDetails> s_Path_TestHook_Overrides; #endif @@ -230,104 +208,6 @@ namespace AppInstaller::Runtime }; } - bool IsRunningInPackagedContext() - { - static bool result = DoesCurrentProcessHaveIdentity(); - return result; - } - - LocIndString GetClientVersion() - { - using namespace std::string_literals; - - // Major and minor come directly from version.h - std::ostringstream strstr; - strstr << VERSION_MAJOR << '.' << VERSION_MINOR << '.'; - - // Build comes from the package for now, if packaged. - if (IsRunningInPackagedContext()) - { - auto version = GetPACKAGE_VERSION(); - - if (!version) - { - // In the extremely unlikely event of a failure, this is merely a sentinel value - // to indicated such. The only other option is to completely prevent execution, - // which seems unnecessary. - return LocIndString{ "error"sv }; - } - - strstr << version->Build; - } - else - { - strstr << VERSION_BUILD; - } - - if (!IsReleaseBuild()) - { - strstr << s_PreviewBuildSuffix; - } - - return LocIndString{ strstr.str() }; - } - - LocIndString GetPackageVersion() - { - using namespace std::string_literals; - - if (IsRunningInPackagedContext()) - { - auto version = GetPACKAGE_VERSION(); - - if (!version) - { - // In the extremely unlikely event of a failure, this is merely a sentinel value - // to indicated such. The only other option is to completely prevent execution, - // which seems unnecessary. - return LocIndString{ "error"sv }; - } - - std::ostringstream strstr; - strstr << GetPackageName() << " v" << version->Major << '.' << version->Minor << '.' << version->Build << '.' << version->Revision; - - return LocIndString{ strstr.str() }; - } - else - { - // Calling code should avoid calling in when this is the case. - return LocIndString{ "none"sv }; - } - } - -#ifndef WINGET_DISABLE_FOR_FUZZING - LocIndString GetOSVersion() - { - winrt::Windows::System::Profile::AnalyticsInfo analyticsInfo{}; - auto versionInfo = analyticsInfo.VersionInfo(); - - uint64_t version = std::stoull(Utility::ConvertToUTF8(versionInfo.DeviceFamilyVersion())); - uint16_t parts[4]; - - for (size_t i = 0; i < ARRAYSIZE(parts); ++i) - { - parts[i] = version & 0xFFFF; - version = version >> 16; - } - - std::ostringstream strstr; - strstr << Utility::ConvertToUTF8(versionInfo.DeviceFamily()) << " v" << parts[3] << '.' << parts[2] << '.' << parts[1] << '.' << parts[0]; - - return LocIndString{ strstr.str() }; - } - - std::string GetOSRegion() - { - winrt::Windows::Globalization::GeographicRegion region; - return Utility::ConvertToUTF8(region.CodeTwoLetter()); - } -#endif - void SetRuntimePathStateName(std::string name) { auto suitablePathPart = MakeSuitablePathPart(name); @@ -713,50 +593,6 @@ namespace AppInstaller::Runtime return tempFilePath; } - bool IsCurrentOSVersionGreaterThanOrEqual(const Utility::Version& version) - { - DWORD versionParts[3] = {}; - - for (size_t i = 0; i < ARRAYSIZE(versionParts) && i < version.GetParts().size(); ++i) - { - versionParts[i] = static_cast<DWORD>(std::min(static_cast<decltype(version.GetParts()[i].Integer)>(std::numeric_limits<DWORD>::max()), version.GetParts()[i].Integer)); - } - - OSVERSIONINFOEXW osVersionInfo{}; - osVersionInfo.dwOSVersionInfoSize = sizeof(osVersionInfo); - osVersionInfo.dwMajorVersion = versionParts[0]; - osVersionInfo.dwMinorVersion = versionParts[1]; - osVersionInfo.dwBuildNumber = versionParts[2]; - osVersionInfo.wServicePackMajor = 0; - osVersionInfo.wServicePackMinor = 0; - - DWORD mask = VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER | VER_SERVICEPACKMAJOR | VER_SERVICEPACKMINOR; - - DWORDLONG conditions = 0; - VER_SET_CONDITION(conditions, VER_MAJORVERSION, VER_GREATER_EQUAL); - VER_SET_CONDITION(conditions, VER_MINORVERSION, VER_GREATER_EQUAL); - VER_SET_CONDITION(conditions, VER_BUILDNUMBER, VER_GREATER_EQUAL); - VER_SET_CONDITION(conditions, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); - VER_SET_CONDITION(conditions, VER_SERVICEPACKMINOR, VER_GREATER_EQUAL); - - BOOL result = VerifyVersionInfoW(&osVersionInfo, mask, conditions); - if (!result) - { - THROW_LAST_ERROR_IF(GetLastError() != ERROR_OLD_WIN_VERSION); - } - return !!result; - } - - bool IsRunningAsAdmin() - { - return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); - } - - bool IsRunningAsSystem() - { - return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID); - } - // Determines whether developer mode is enabled. // Does not account for the group policy value which takes precedence over this registry value. bool IsDevModeEnabled() @@ -773,15 +609,6 @@ namespace AppInstaller::Runtime } } - constexpr bool IsReleaseBuild() - { -#ifdef WINGET_ENABLE_RELEASE_BUILD - return true; -#else - return false; -#endif - } - // Using "standard" user agent format // Keeping `winget-cli` for historical reasons Utility::LocIndString GetDefaultUserAgent() diff --git a/src/AppInstallerCommonCore/Telemetry/TraceLogging.h b/src/AppInstallerCommonCore/Telemetry/TraceLogging.h @@ -3,10 +3,10 @@ #pragma once -#include "WinEventLogLevels.h" +#include <Telemetry/WinEventLogLevels.h> #include <TraceLoggingProvider.h> -#include "MicrosoftTelemetry.h" +#include <Telemetry/MicrosoftTelemetry.h> // Keywords #define KEYWORD_REPEATER 0x0000000000000001 diff --git a/src/AppInstallerCommonCore/ThreadGlobals.cpp b/src/AppInstallerCommonCore/ThreadGlobals.cpp @@ -21,6 +21,11 @@ namespace AppInstaller::ThreadLocalStorage return *(m_pDiagnosticLogger); } + void* WingetThreadGlobals::GetTelemetryObject() + { + return m_pTelemetryLogger.get(); + } + TelemetryTraceLogger& WingetThreadGlobals::GetTelemetryLogger() { return *(m_pTelemetryLogger); diff --git a/src/AppInstallerCommonCore/TraceLogger.cpp b/src/AppInstallerCommonCore/TraceLogger.cpp @@ -24,7 +24,7 @@ namespace AppInstaller::Logging // Just eat any exceptions here; better to lose logs than functionality } - void TraceLogger::WriteDirect(std::string_view message) noexcept try + void TraceLogger::WriteDirect(Channel, Level, std::string_view message) noexcept try { TraceLoggingWriteActivity(g_hTraceProvider, "Diagnostics", diff --git a/src/AppInstallerSharedLib/AppInstallerLogging.cpp b/src/AppInstallerSharedLib/AppInstallerLogging.cpp @@ -2,7 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/AppInstallerLogging.h" - +#include "Public/AppInstallerStrings.h" #include "Public/AppInstallerDateTime.h" #include "Public/winget/SharedThreadGlobals.h" @@ -135,7 +135,7 @@ namespace AppInstaller::Logging { for (auto& logger : m_loggers) { - logger->WriteDirect(message); + logger->WriteDirect(channel, level, message); } } } @@ -165,3 +165,19 @@ std::ostream& operator<<(std::ostream& out, const std::chrono::system_clock::tim AppInstaller::Utility::OutputTimePoint(out, time); return out; } + +std::ostream& operator<<(std::ostream& out, const GUID& guid) +{ + wchar_t buffer[256]; + + if (StringFromGUID2(guid, buffer, ARRAYSIZE(buffer))) + { + out << AppInstaller::Utility::ConvertToUTF8(buffer); + } + else + { + out << "error"; + } + + return out; +} diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -294,9 +294,12 @@ <ClInclude Include="Public\AppInstallerStrings.h" /> <ClInclude Include="Public\AppInstallerLogging.h" /> <ClInclude Include="Public\AppInstallerVersions.h" /> + <ClInclude Include="Public\Telemetry\MicrosoftTelemetry.h" /> + <ClInclude Include="Public\Telemetry\WinEventLogLevels.h" /> <ClInclude Include="Public\winget\JsonSchemaValidation.h" /> <ClInclude Include="Public\winget\LocIndependent.h" /> <ClInclude Include="Public\winget\Resources.h" /> + <ClInclude Include="Public\winget\Runtime.h" /> <ClInclude Include="Public\winget\SharedThreadGlobals.h" /> <ClInclude Include="Public\winget\Yaml.h" /> <ClInclude Include="YamlWrapper.h" /> @@ -311,6 +314,7 @@ <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> + <ClCompile Include="Runtime.cpp" /> <ClCompile Include="SHA256.cpp" /> <ClCompile Include="SharedThreadGlobals.cpp" /> <ClCompile Include="Versions.cpp" /> diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -15,6 +15,9 @@ <Filter Include="Public\winget"> <UniqueIdentifier>{41035fd6-dc74-4464-b9b1-4ffe95d6789c}</UniqueIdentifier> </Filter> + <Filter Include="Public\Telemetry - Do Not Modify"> + <UniqueIdentifier>{3a5b2424-6c80-4edc-85bc-f371f2e93a33}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -59,6 +62,15 @@ <ClInclude Include="Public\AppInstallerVersions.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="Public\Telemetry\MicrosoftTelemetry.h"> + <Filter>Public\Telemetry - Do Not Modify</Filter> + </ClInclude> + <ClInclude Include="Public\Telemetry\WinEventLogLevels.h"> + <Filter>Public\Telemetry - Do Not Modify</Filter> + </ClInclude> + <ClInclude Include="Public\winget\Runtime.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -97,6 +109,9 @@ <ClCompile Include="Versions.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Runtime.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -166,14 +166,15 @@ #define WINGET_CONFIG_ERROR_WARNING_NOT_ACCEPTED ((HRESULT)0x8A15C00B) // Configuration Processor Errors -#define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) -#define WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY ((HRESULT)0x8A15C102) -#define WINGET_CONFIG_ERROR_UNIT_MULTIPLE_MATCHES ((HRESULT)0x8A15C103) -#define WINGET_CONFIG_ERROR_UNIT_INVOKE_GET ((HRESULT)0x8A15C104) -#define WINGET_CONFIG_ERROR_UNIT_INVOKE_TEST ((HRESULT)0x8A15C105) -#define WINGET_CONFIG_ERROR_UNIT_INVOKE_SET ((HRESULT)0x8A15C106) -#define WINGET_CONFIG_ERROR_UNIT_MODULE_CONFLICT ((HRESULT)0x8A15C107) -#define WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE ((HRESULT)0x8A15C108) +#define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) +#define WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY ((HRESULT)0x8A15C102) +#define WINGET_CONFIG_ERROR_UNIT_MULTIPLE_MATCHES ((HRESULT)0x8A15C103) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_GET ((HRESULT)0x8A15C104) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_TEST ((HRESULT)0x8A15C105) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_SET ((HRESULT)0x8A15C106) +#define WINGET_CONFIG_ERROR_UNIT_MODULE_CONFLICT ((HRESULT)0x8A15C107) +#define WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE ((HRESULT)0x8A15C108) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT ((HRESULT)0x8A15C109) namespace AppInstaller { diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h @@ -85,7 +85,7 @@ namespace AppInstaller::Logging virtual void Write(Channel channel, Level level, std::string_view message) noexcept = 0; // Informs the logger of the given log with the intention that no buffering occurs (in winget code). - virtual void WriteDirect(std::string_view message) noexcept = 0; + virtual void WriteDirect(Channel channel, Level level, std::string_view message) noexcept = 0; }; // This type contains the set of loggers that diagnostic logging will be sent to. @@ -187,5 +187,5 @@ namespace AppInstaller::Logging }; } -// Enable output of system_clock time_points. std::ostream& operator<<(std::ostream& out, const std::chrono::system_clock::time_point& time); +std::ostream& operator<<(std::ostream& out, const GUID& time); diff --git a/src/AppInstallerCommonCore/Telemetry/MicrosoftTelemetry.h b/src/AppInstallerSharedLib/Public/Telemetry/MicrosoftTelemetry.h diff --git a/src/AppInstallerCommonCore/Telemetry/WinEventLogLevels.h b/src/AppInstallerSharedLib/Public/Telemetry/WinEventLogLevels.h diff --git a/src/AppInstallerSharedLib/Public/winget/Runtime.h b/src/AppInstallerSharedLib/Public/winget/Runtime.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerVersions.h> +#include <winget/LocIndependent.h> + +#include <filesystem> +#include <memory> +#include <string> +#include <string_view> + +namespace AppInstaller::Runtime +{ + // Determines whether the process is running in a packaged context or not. + bool IsRunningInPackagedContext(); + + // Determines the current version of the client and returns it. + Utility::LocIndString GetClientVersion(); + + // Determines the current version of the package if running in a packaged context. + Utility::LocIndString GetPackageVersion(); + + // Gets a string representation of the OS version for debugging purposes. + Utility::LocIndString GetOSVersion(); + + // Gets the OS region. + // This can be used as the current market. + std::string GetOSRegion(); + + // Determines whether the current OS version is >= the given one. + // We treat the given Version struct as a standard 4 part Windows OS version. + bool IsCurrentOSVersionGreaterThanOrEqual(const Utility::Version& version); + + // Determines whether the process is running with administrator privileges. + bool IsRunningAsAdmin(); + + // Determines whether the process is running with local system context. + bool IsRunningAsSystem(); + + // Returns true if this is a release build; false if not. + inline constexpr bool IsReleaseBuild(); +} diff --git a/src/AppInstallerSharedLib/Public/winget/SharedThreadGlobals.h b/src/AppInstallerSharedLib/Public/winget/SharedThreadGlobals.h @@ -3,11 +3,6 @@ #pragma once #include <AppInstallerLogging.h> -namespace AppInstaller::Logging -{ - struct TelemetryTraceLogger; -} - namespace AppInstaller::ThreadLocalStorage { struct PreviousThreadGlobals; @@ -20,7 +15,7 @@ namespace AppInstaller::ThreadLocalStorage virtual AppInstaller::Logging::DiagnosticLogger& GetDiagnosticLogger() = 0; - virtual AppInstaller::Logging::TelemetryTraceLogger& GetTelemetryLogger() = 0; + virtual void* GetTelemetryObject() = 0; // Set Globals for Current Thread // Return RAII object with it's ownership to set the AppInstaller ThreadLocalStorage back to previous state diff --git a/src/AppInstallerSharedLib/Runtime.cpp b/src/AppInstallerSharedLib/Runtime.cpp @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include <binver/version.h> +#include "Public/winget/Runtime.h" +#include "Public/AppInstallerLogging.h" +#include "Public/AppInstallerStrings.h" + + +namespace AppInstaller::Runtime +{ + using namespace Utility; + + namespace + { + using namespace std::string_view_literals; + constexpr std::string_view s_PreviewBuildSuffix = "-preview"sv; + + // Gets a boolean indicating whether the current process has identity. + bool DoesCurrentProcessHaveIdentity() + { + UINT32 length = 0; + LONG result = GetPackageFamilyName(GetCurrentProcess(), &length, nullptr); + return (result != APPMODEL_ERROR_NO_PACKAGE); + } + + std::unique_ptr<byte[]> GetPACKAGE_ID() + { + UINT32 bufferLength = 0; + LONG gcpiResult = GetCurrentPackageId(&bufferLength, nullptr); + THROW_HR_IF(E_UNEXPECTED, gcpiResult != ERROR_INSUFFICIENT_BUFFER); + + std::unique_ptr<byte[]> buffer = std::make_unique<byte[]>(bufferLength); + + gcpiResult = GetCurrentPackageId(&bufferLength, buffer.get()); + if (FAILED_WIN32_LOG(gcpiResult)) + { + return {}; + } + + return buffer; + } + + // Gets the package name; only succeeds if running in a packaged context. + std::string GetPackageName() + { + std::unique_ptr<byte[]> buffer = GetPACKAGE_ID(); + if (!buffer) + { + return {}; + } + + PACKAGE_ID* packageId = reinterpret_cast<PACKAGE_ID*>(buffer.get()); + return Utility::ConvertToUTF8(packageId->name); + } + + // Gets the package version; only succeeds if running in a packaged context. + std::optional<PACKAGE_VERSION> GetPACKAGE_VERSION() + { + std::unique_ptr<byte[]> buffer = GetPACKAGE_ID(); + if (!buffer) + { + return {}; + } + + PACKAGE_ID* packageId = reinterpret_cast<PACKAGE_ID*>(buffer.get()); + return packageId->version; + } + } + + bool IsRunningInPackagedContext() + { + static bool result = DoesCurrentProcessHaveIdentity(); + return result; + } + + LocIndString GetClientVersion() + { + std::ostringstream strstr; + strstr << VERSION_MAJOR << '.' << VERSION_MINOR << '.' << VERSION_BUILD; + + if (!IsReleaseBuild()) + { + strstr << s_PreviewBuildSuffix; + } + + return LocIndString{ strstr.str() }; + } + + LocIndString GetPackageVersion() + { + using namespace std::string_literals; + + if (IsRunningInPackagedContext()) + { + auto version = GetPACKAGE_VERSION(); + + if (!version) + { + // In the extremely unlikely event of a failure, this is merely a sentinel value + // to indicated such. The only other option is to completely prevent execution, + // which seems unnecessary. + return LocIndString{ "error"sv }; + } + + std::ostringstream strstr; + strstr << GetPackageName() << " v" << version->Major << '.' << version->Minor << '.' << version->Build << '.' << version->Revision; + + return LocIndString{ strstr.str() }; + } + else + { + // Calling code should avoid calling in when this is the case. + return LocIndString{ "none"sv }; + } + } + +#ifndef WINGET_DISABLE_FOR_FUZZING + LocIndString GetOSVersion() + { + winrt::Windows::System::Profile::AnalyticsInfo analyticsInfo{}; + auto versionInfo = analyticsInfo.VersionInfo(); + + uint64_t version = std::stoull(Utility::ConvertToUTF8(versionInfo.DeviceFamilyVersion())); + uint16_t parts[4]; + + for (size_t i = 0; i < ARRAYSIZE(parts); ++i) + { + parts[i] = version & 0xFFFF; + version = version >> 16; + } + + std::ostringstream strstr; + strstr << Utility::ConvertToUTF8(versionInfo.DeviceFamily()) << " v" << parts[3] << '.' << parts[2] << '.' << parts[1] << '.' << parts[0]; + + return LocIndString{ strstr.str() }; + } + + std::string GetOSRegion() + { + winrt::Windows::Globalization::GeographicRegion region; + return Utility::ConvertToUTF8(region.CodeTwoLetter()); + } +#endif + + bool IsCurrentOSVersionGreaterThanOrEqual(const Utility::Version& version) + { + DWORD versionParts[3] = {}; + + for (size_t i = 0; i < ARRAYSIZE(versionParts) && i < version.GetParts().size(); ++i) + { + versionParts[i] = static_cast<DWORD>(std::min(static_cast<decltype(version.GetParts()[i].Integer)>(std::numeric_limits<DWORD>::max()), version.GetParts()[i].Integer)); + } + + OSVERSIONINFOEXW osVersionInfo{}; + osVersionInfo.dwOSVersionInfoSize = sizeof(osVersionInfo); + osVersionInfo.dwMajorVersion = versionParts[0]; + osVersionInfo.dwMinorVersion = versionParts[1]; + osVersionInfo.dwBuildNumber = versionParts[2]; + osVersionInfo.wServicePackMajor = 0; + osVersionInfo.wServicePackMinor = 0; + + DWORD mask = VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER | VER_SERVICEPACKMAJOR | VER_SERVICEPACKMINOR; + + DWORDLONG conditions = 0; + VER_SET_CONDITION(conditions, VER_MAJORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditions, VER_MINORVERSION, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditions, VER_BUILDNUMBER, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditions, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL); + VER_SET_CONDITION(conditions, VER_SERVICEPACKMINOR, VER_GREATER_EQUAL); + + BOOL result = VerifyVersionInfoW(&osVersionInfo, mask, conditions); + if (!result) + { + THROW_LAST_ERROR_IF(GetLastError() != ERROR_OLD_WIN_VERSION); + } + return !!result; + } + + bool IsRunningAsAdmin() + { + return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + } + + bool IsRunningAsSystem() + { + return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID); + } + + constexpr bool IsReleaseBuild() + { +#ifdef WINGET_ENABLE_RELEASE_BUILD + return true; +#else + return false; +#endif + } +} diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h @@ -4,6 +4,7 @@ #define NOMINMAX #include <Windows.h> +#include <appmodel.h> #include <icu.h> #define YAML_DECLARE_STATIC @@ -55,4 +56,6 @@ #include <winrt/Windows.ApplicationModel.Resources.h> #include <winrt/Windows.Foundation.h> +#include <winrt/Windows.Globalization.h> +#include <winrt/Windows.System.Profile.h> #endif diff --git a/src/Microsoft.Management.Configuration.Processor/DscModules/DscModuleV2.cs b/src/Microsoft.Management.Configuration.Processor/DscModules/DscModuleV2.cs @@ -95,21 +95,30 @@ namespace Microsoft.Management.Configuration.Processor.DscModule string name, ModuleSpecification? moduleSpecification) { - var getResult = pwsh.AddCommand(this.InvokeDscResourceCmd) + PSObject? getResult = null; + + try + { + getResult = pwsh.AddCommand(this.InvokeDscResourceCmd) .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) .AddParameter(Parameters.Method, DscMethods.Get) .InvokeAndStopOnError() .FirstOrDefault(); + } + catch (System.Exception ex) + { + throw new InvokeDscResourceException(InvokeDscResourceException.Get, name, moduleSpecification, ex); + } string? errorMessage = pwsh.GetErrorMessage(); if (errorMessage is not null) { - throw new InvokeDscResourceGetException(name, moduleSpecification, errorMessage); + throw new InvokeDscResourceException(InvokeDscResourceException.Get, name, moduleSpecification, errorMessage); } if (getResult is null) { - throw new InvokeDscResourceGetException(name, moduleSpecification); + throw new InvokeDscResourceException(InvokeDscResourceException.Get, name, moduleSpecification); } // Script based resource. @@ -138,22 +147,31 @@ namespace Microsoft.Management.Configuration.Processor.DscModule { // Returned type is InvokeDscResourceTestResult which is a PowerShell classed defined // in PSDesiredStateConfiguration.psm1. - dynamic? testResult = pwsh.AddCommand(this.InvokeDscResourceCmd) - .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) - .AddParameter(Parameters.Method, DscMethods.Test) - .InvokeAndStopOnError() - .FirstOrDefault(); + dynamic? testResult = null; + + try + { + testResult = pwsh.AddCommand(this.InvokeDscResourceCmd) + .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) + .AddParameter(Parameters.Method, DscMethods.Test) + .InvokeAndStopOnError() + .FirstOrDefault(); + } + catch (System.Exception ex) + { + throw new InvokeDscResourceException(InvokeDscResourceException.Test, name, moduleSpecification, ex); + } string? errorMessage = pwsh.GetErrorMessage(); if (errorMessage is not null) { - throw new InvokeDscResourceTestException(name, moduleSpecification, errorMessage); + throw new InvokeDscResourceException(InvokeDscResourceException.Test, name, moduleSpecification, errorMessage); } if (testResult is null || !TypeHelpers.PropertyWithTypeExists<bool>(testResult, InDesiredState)) { - throw new InvokeDscResourceTestException(name, moduleSpecification); + throw new InvokeDscResourceException(InvokeDscResourceException.Test, name, moduleSpecification); } return testResult?.InDesiredState; @@ -168,22 +186,31 @@ namespace Microsoft.Management.Configuration.Processor.DscModule { // Returned type is InvokeDscResourceSetResult which is a PowerShell classed defined // in PSDesiredStateConfiguration.psm1. - dynamic? setResult = pwsh.AddCommand(this.InvokeDscResourceCmd) - .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) - .AddParameter(Parameters.Method, DscMethods.Set) - .InvokeAndStopOnError() - .FirstOrDefault(); + dynamic? setResult = null; + + try + { + setResult = pwsh.AddCommand(this.InvokeDscResourceCmd) + .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) + .AddParameter(Parameters.Method, DscMethods.Set) + .InvokeAndStopOnError() + .FirstOrDefault(); + } + catch (System.Exception ex) + { + throw new InvokeDscResourceException(InvokeDscResourceException.Set, name, moduleSpecification, ex); + } string? errorMessage = pwsh.GetErrorMessage(); if (errorMessage is not null) { - throw new InvokeDscResourceSetException(name, moduleSpecification, errorMessage); + throw new InvokeDscResourceException(InvokeDscResourceException.Set, name, moduleSpecification, errorMessage, pwsh.ContainsPropertyError()); } if (setResult is null || !TypeHelpers.PropertyWithTypeExists<bool>(setResult, RebootRequired)) { - throw new InvokeDscResourceSetException(name, moduleSpecification); + throw new InvokeDscResourceException(InvokeDscResourceException.Set, name, moduleSpecification); } return setResult?.RebootRequired; diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -12,6 +12,11 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions internal static class ErrorCodes { /// <summary> + /// Corresponds to E_UNEXPECTED; this code path was reached without the developer realizing it was possible. + /// </summary> + internal const int Unexpected = unchecked((int)0x8000ffff); + + /// <summary> /// The module of the unit was installed, but the unit was not found. /// </summary> internal const int WinGetConfigUnitNotFound = unchecked((int)0x8A15C101); @@ -27,17 +32,17 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions internal const int WinGetConfigUnitMultipleMatches = unchecked((int)0x8A15C103); /// <summary> - /// Internal error calling Invoke-DscResource Get. + /// Unit error calling Invoke-DscResource Get. /// </summary> internal const int WinGetConfigUnitInvokeGet = unchecked((int)0x8A15C104); /// <summary> - /// Internal error calling Invoke-DscResource Test. + /// Unit error calling Invoke-DscResource Test. /// </summary> internal const int WinGetConfigUnitInvokeTest = unchecked((int)0x8A15C105); /// <summary> - /// Internal error calling Invoke-DscResource Set. + /// Unit error calling Invoke-DscResource Set. /// </summary> internal const int WinGetConfigUnitInvokeSet = unchecked((int)0x8A15C106); @@ -50,5 +55,10 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions /// The module where the DSC resource is implemented cannot be imported. /// </summary> internal const int WinGetConfigUnitImportModule = unchecked((int)0x8A15C108); + + /// <summary> + /// The unit returned an invalid result. + /// </summary> + internal const int WinGetConfigUnitInvokeInvalidResult = unchecked((int)0x8A15C109); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/IConfigurationUnitResultException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/IConfigurationUnitResultException.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IConfigurationUnitResultException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// An interface that enables an exception to expose information appropriate for a unit result. + /// </summary> + internal interface IConfigurationUnitResultException + { + /// <summary> + /// Gets a value indicating the source of the result. + /// </summary> + public ConfigurationUnitResultSource ResultSource { get; } + + /// <summary> + /// Gets the description of the result. + /// </summary> + public string Description { get; } + + /// <summary> + /// Gets the details for the result. + /// </summary> + public string Details { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceException.cs @@ -0,0 +1,163 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvokeDscResourceException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.Management.Configuration; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// A call to Invoke-DscResource failed unexpectedly. + /// </summary> + internal class InvokeDscResourceException : Exception, IConfigurationUnitResultException + { + /// <summary> + /// The string for the Get method. + /// </summary> + public const string Get = "Get"; + + /// <summary> + /// The string for the Set method. + /// </summary> + public const string Set = "Set"; + + /// <summary> + /// The string for the Test method. + /// </summary> + public const string Test = "Test"; + + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceException"/> class. + /// Use this constructor when no error is generated by the invoke and the result is not a valid value. + /// </summary> + /// <param name="method">Method.</param> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + public InvokeDscResourceException(string method, string resourceName, ModuleSpecification? module) + : base(CreateMessage(method, resourceName, module, null)) + { + // No message means that the invoke returned an invalid result. + this.HResult = ErrorCodes.WinGetConfigUnitInvokeInvalidResult; + this.Method = method; + this.ResourceName = resourceName; + this.Module = module; + } + + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceException"/> class. + /// Use this constructor when the invoke fails with an error message. + /// </summary> + /// <param name="method">Method.</param> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + /// <param name="message">Message.</param> + /// <param name="configurationSetSource">If true, the source of this error is set to be the configuration.</param> + public InvokeDscResourceException(string method, string resourceName, ModuleSpecification? module, string message, bool configurationSetSource = false) + : base(CreateMessage(method, resourceName, module, message)) + { + this.HResult = GetHRForMethod(method); + this.Method = method; + this.ResourceName = resourceName; + this.Module = module; + this.Description = message; + + if (configurationSetSource) + { + this.ResultSource = ConfigurationUnitResultSource.ConfigurationSet; + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceException"/> class. + /// Use this constructor when the invoke fails with an exception. + /// </summary> + /// <param name="method">Method.</param> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + /// <param name="inner">The invoke exception.</param> + public InvokeDscResourceException(string method, string resourceName, ModuleSpecification? module, Exception inner) + : base(CreateMessage(method, resourceName, module, inner.Message), inner) + { + this.HResult = GetHRForMethod(method); + this.Method = method; + this.ResourceName = resourceName; + this.Module = module; + this.Description = (inner as RuntimeException)?.ErrorRecord.ToString() ?? inner.Message; + } + + /// <summary> + /// Gets the invoke method. + /// </summary> + public string Method { get; } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string ResourceName { get; } + + /// <summary> + /// Gets the module, if any. + /// </summary> + public ModuleSpecification? Module { get; } + + /// <summary> + /// Gets a value indicating the source of the result. + /// </summary> + public ConfigurationUnitResultSource ResultSource { get; } = ConfigurationUnitResultSource.UnitProcessing; + + /// <summary> + /// Gets the description of the result. + /// </summary> + public string Description { get; } = string.Empty; + + /// <summary> + /// Gets the details for the result. + /// </summary> + public string Details + { + get + { + RuntimeException? re = this.InnerException as RuntimeException; + if (re != null) + { + return re.ErrorRecord.ScriptStackTrace; + } + + return this.ToString(); + } + } + + /// <summary> + /// Gets the HRESULT value for the given method. + /// </summary> + /// <param name="method">The method.</param> + /// <returns>The HRESULT for the method.</returns> + private static int GetHRForMethod(string method) + { + switch (method) + { + case Get: return ErrorCodes.WinGetConfigUnitInvokeGet; + case Set: return ErrorCodes.WinGetConfigUnitInvokeSet; + case Test: return ErrorCodes.WinGetConfigUnitInvokeTest; + } + + return ErrorCodes.Unexpected; + } + + private static string CreateMessage(string method, string resourceName, ModuleSpecification? module, string? message) + { + string result = $"Failed when calling `{method}` for resource: {resourceName} [{module?.ToString() ?? "<no module>"}]"; + if (message != null) + { + result += $" Message: '{message}'"; + } + + return result; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceGetException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceGetException.cs @@ -1,65 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="InvokeDscResourceGetException.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.Management.Configuration.Processor.Exceptions -{ - using System; - using Microsoft.PowerShell.Commands; - - /// <summary> - /// A call to Invoke-DscResource Get failed unexpectedly. - /// </summary> - internal class InvokeDscResourceGetException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="InvokeDscResourceGetException"/> class. - /// </summary> - /// <param name="resourceName">Resource name.</param> - /// <param name="module">Optional module.</param> - public InvokeDscResourceGetException(string resourceName, ModuleSpecification? module) - : base(CreateMessage(resourceName, module, null)) - { - this.HResult = ErrorCodes.WinGetConfigUnitInvokeGet; - this.ResourceName = resourceName; - this.Module = module; - } - - /// <summary> - /// Initializes a new instance of the <see cref="InvokeDscResourceGetException"/> class. - /// </summary> - /// <param name="resourceName">Resource name.</param> - /// <param name="module">Optional module.</param> - /// <param name="message">Message.</param> - public InvokeDscResourceGetException(string resourceName, ModuleSpecification? module, string message) - : base(CreateMessage(resourceName, module, message)) - { - this.HResult = ErrorCodes.WinGetConfigUnitInvokeGet; - this.ResourceName = resourceName; - this.Module = module; - } - - /// <summary> - /// Gets the resource name. - /// </summary> - public string ResourceName { get; } - - /// <summary> - /// Gets the module, if any. - /// </summary> - public ModuleSpecification? Module { get; } - - private static string CreateMessage(string resourceName, ModuleSpecification? module, string? message) - { - string result = $"Failed when calling `Get` for resource: {resourceName} [{module?.ToString() ?? "<no module>"}]"; - if (message != null) - { - result += $" Message: '{message}'"; - } - - return result; - } - } -} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceSetException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceSetException.cs @@ -1,65 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="InvokeDscResourceSetException.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.Management.Configuration.Processor.Exceptions -{ - using System; - using Microsoft.PowerShell.Commands; - - /// <summary> - /// A call to Invoke-DscResource Set failed unexpectedly. - /// </summary> - internal class InvokeDscResourceSetException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="InvokeDscResourceSetException"/> class. - /// </summary> - /// <param name="resourceName">Resource name.</param> - /// <param name="module">Optional module.</param> - public InvokeDscResourceSetException(string resourceName, ModuleSpecification? module) - : base(CreateMessage(resourceName, module, null)) - { - this.HResult = ErrorCodes.WinGetConfigUnitInvokeSet; - this.ResourceName = resourceName; - this.Module = module; - } - - /// <summary> - /// Initializes a new instance of the <see cref="InvokeDscResourceSetException"/> class. - /// </summary> - /// <param name="resourceName">Resource name.</param> - /// <param name="module">Optional module.</param> - /// <param name="message">Message.</param> - public InvokeDscResourceSetException(string resourceName, ModuleSpecification? module, string message) - : base(CreateMessage(resourceName, module, message)) - { - this.HResult = ErrorCodes.WinGetConfigUnitInvokeSet; - this.ResourceName = resourceName; - this.Module = module; - } - - /// <summary> - /// Gets the resource name. - /// </summary> - public string ResourceName { get; } - - /// <summary> - /// Gets the module, if any. - /// </summary> - public ModuleSpecification? Module { get; } - - private static string CreateMessage(string resourceName, ModuleSpecification? module, string? message) - { - string result = $"Failed when calling `Set` for resource: {resourceName} [{module?.ToString() ?? "<no module>"}]"; - if (message != null) - { - result += $" Message: '{message}'"; - } - - return result; - } - } -} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceTestException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceTestException.cs @@ -1,65 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="InvokeDscResourceTestException.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.Management.Configuration.Processor.Exceptions -{ - using System; - using Microsoft.PowerShell.Commands; - - /// <summary> - /// A call to Invoke-DscResource Test failed unexpectedly. - /// </summary> - internal class InvokeDscResourceTestException : Exception - { - /// <summary> - /// Initializes a new instance of the <see cref="InvokeDscResourceTestException"/> class. - /// </summary> - /// <param name="resourceName">Resource name.</param> - /// <param name="module">Optional module.</param> - public InvokeDscResourceTestException(string resourceName, ModuleSpecification? module) - : base(CreateMessage(resourceName, module, null)) - { - this.HResult = ErrorCodes.WinGetConfigUnitInvokeTest; - this.ResourceName = resourceName; - this.Module = module; - } - - /// <summary> - /// Initializes a new instance of the <see cref="InvokeDscResourceTestException"/> class. - /// </summary> - /// <param name="resourceName">Resource name.</param> - /// <param name="module">Optional module.</param> - /// <param name="message">Message.</param> - public InvokeDscResourceTestException(string resourceName, ModuleSpecification? module, string message) - : base(CreateMessage(resourceName, module, message)) - { - this.HResult = ErrorCodes.WinGetConfigUnitInvokeTest; - this.ResourceName = resourceName; - this.Module = module; - } - - /// <summary> - /// Gets the resource name. - /// </summary> - public string ResourceName { get; } - - /// <summary> - /// Gets the module, if any. - /// </summary> - public ModuleSpecification? Module { get; } - - private static string CreateMessage(string resourceName, ModuleSpecification? module, string? message) - { - string result = $"Failed when calling `Test` for resource: {resourceName} [{module?.ToString() ?? "<no module>"}]"; - if (message != null) - { - result += $" Message: '{message}'"; - } - - return result; - } - } -} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs @@ -7,6 +7,7 @@ namespace Microsoft.Management.Configuration.Processor.Extensions { using System.Collections.ObjectModel; + using System.Linq; using System.Management.Automation; using System.Text; @@ -66,5 +67,31 @@ namespace Microsoft.Management.Configuration.Processor.Extensions return null; } + + /// <summary> + /// Determines if the given shell contains a property error, meaning that the source of this error is the + /// configuration values and not the configuration unit itself. + /// </summary> + /// <param name="pwsh">The shell to inspect.</param> + /// <returns>True if it only contains property errors; false otherwise.</returns> + public static bool ContainsPropertyError(this PowerShell pwsh) + { + if (!pwsh.HadErrors) + { + return false; + } + + bool result = true; + + foreach (ErrorRecord? error in pwsh.Streams.Error) + { + if (error?.FullyQualifiedErrorId == "PropertyAssignmentException") + { + result = result && true; + } + } + + return result; + } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs @@ -208,7 +208,7 @@ namespace Microsoft.Management.Configuration.Processor.Set // resources because they will call a method on a null obj. It is easier to just fail here. // The exception being thrown will have the correct details (user needs to call Unblock-File) // instead of the cryptic Invoke with 0 arguments. - if (dscResourceInfo.Path is not null) + if (!string.IsNullOrEmpty(dscResourceInfo.Path)) { try { diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs @@ -8,12 +8,11 @@ namespace Microsoft.Management.Configuration.Processor.Unit { using System; using System.Collections.Generic; - using System.Management.Automation; using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.Exceptions; using Microsoft.Management.Configuration.Processor.Extensions; using Microsoft.Management.Configuration.Processor.Helpers; using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; - using Microsoft.PowerShell.Commands; /// <summary> /// Provides access to a specific configuration unit within the runtime. @@ -61,6 +60,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit this.OnDiagnostics(DiagnosticLevel.Verbose, $"Invoking `Get` for resource: {this.unitResource.UnitInternal.ToIdentifyingString()}..."); var result = new GetSettingsResult(); + try { result.Settings = this.processorEnvironment.InvokeGetResource( @@ -68,24 +68,9 @@ namespace Microsoft.Management.Configuration.Processor.Unit this.unitResource.ResourceName, this.unitResource.Module); } - catch (Exception e) when (e is RuntimeException || - e is WriteErrorException) - { - RuntimeException? re = e as RuntimeException; - if (re != null) - { - this.OnDiagnostics(DiagnosticLevel.Error, $"An error occurred within the configuration unit when attempting `Get`:\n{re.ErrorRecord.ToString()}\n{re.ErrorRecord.ScriptStackTrace}"); - } - - this.OnDiagnostics(DiagnosticLevel.Verbose, e.ToString()); - var inner = e.GetMostInnerException(); - result.ResultInformation.ResultCode = inner; - result.ResultInformation.Description = e.ToString(); - } catch (Exception e) { - this.OnDiagnostics(DiagnosticLevel.Error, e.ToString()); - throw; + this.ExtractExceptionInformation(e, result.ResultInformation); } this.OnDiagnostics(DiagnosticLevel.Verbose, $"... done invoking `Get`."); @@ -118,24 +103,9 @@ namespace Microsoft.Management.Configuration.Processor.Unit result.TestResult = testResult ? ConfigurationTestResult.Positive : ConfigurationTestResult.Negative; } - catch (Exception e) when (e is RuntimeException || - e is WriteErrorException) - { - RuntimeException? re = e as RuntimeException; - if (re != null) - { - this.OnDiagnostics(DiagnosticLevel.Error, $"An error occurred within the configuration unit when attempting `Test`:\n{re.ErrorRecord.ToString()}\n{re.ErrorRecord.ScriptStackTrace}"); - } - - this.OnDiagnostics(DiagnosticLevel.Verbose, e.ToString()); - var inner = e.GetMostInnerException(); - result.ResultInformation.ResultCode = inner; - result.ResultInformation.Description = e.ToString(); - } catch (Exception e) { - this.OnDiagnostics(DiagnosticLevel.Error, e.ToString()); - throw; + this.ExtractExceptionInformation(e, result.ResultInformation); } this.OnDiagnostics(DiagnosticLevel.Verbose, $"... done invoking `Test`."); @@ -166,30 +136,37 @@ namespace Microsoft.Management.Configuration.Processor.Unit this.unitResource.ResourceName, this.unitResource.Module); } - catch (Exception e) when (e is RuntimeException || - e is WriteErrorException) - { - RuntimeException? re = e as RuntimeException; - if (re != null) - { - this.OnDiagnostics(DiagnosticLevel.Error, $"An error occurred within the configuration unit when attempting `Set`:\n{re.ErrorRecord.ToString()}\n{re.ErrorRecord.ScriptStackTrace}"); - } - - this.OnDiagnostics(DiagnosticLevel.Verbose, e.ToString()); - var inner = e.GetMostInnerException(); - result.ResultInformation.ResultCode = inner; - result.ResultInformation.Description = e.ToString(); - } catch (Exception e) { - this.OnDiagnostics(DiagnosticLevel.Error, e.ToString()); - throw; + this.ExtractExceptionInformation(e, result.ResultInformation); } this.OnDiagnostics(DiagnosticLevel.Verbose, $"... done invoking `Apply`."); return result; } + private void ExtractExceptionInformation(Exception e, ConfigurationUnitResultInformation resultInformation) + { + this.OnDiagnostics(DiagnosticLevel.Verbose, e.ToString()); + + IConfigurationUnitResultException? configurationUnitResultException = e as IConfigurationUnitResultException; + if (configurationUnitResultException != null) + { + resultInformation.ResultCode = e; + resultInformation.Description = configurationUnitResultException.Description; + resultInformation.Details = configurationUnitResultException.Details; + resultInformation.ResultSource = configurationUnitResultException.ResultSource; + } + else + { + var inner = e.GetMostInnerException(); + resultInformation.ResultCode = inner; + resultInformation.Description = e.Message; + resultInformation.Details = e.ToString(); + resultInformation.ResultSource = ConfigurationUnitResultSource.Internal; + } + } + private void OnDiagnostics(DiagnosticLevel level, string message) { this.SetProcessorFactory?.OnDiagnostics(level, message); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs @@ -7,8 +7,11 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers { using System; + using Microsoft.CodeAnalysis.Emit; using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Windows.Storage.Streams; + using Xunit; using Xunit.Abstractions; /// <summary> @@ -16,8 +19,6 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers /// </summary> public class ConfigurationProcessorTestBase { - private readonly DiagnosticsEventSink diagnosticsEventSink; - /// <summary> /// Initializes a new instance of the <see cref="ConfigurationProcessorTestBase"/> class. /// </summary> @@ -27,10 +28,15 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers { this.Fixture = fixture; this.Log = log; - this.diagnosticsEventSink = new DiagnosticsEventSink(fixture, log); + this.EventSink = new DiagnosticsEventSink(fixture, log); } /// <summary> + /// Gets the event sink for this test base. + /// </summary> + protected DiagnosticsEventSink EventSink { get; private set; } + + /// <summary> /// Gets the test fixture. /// </summary> protected UnitTestFixture Fixture { get; private init; } @@ -48,7 +54,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers protected ConfigurationProcessor CreateConfigurationProcessorWithDiagnostics(IConfigurationSetProcessorFactory? factory = null) { ConfigurationProcessor result = new ConfigurationProcessor(factory); - result.Diagnostics += this.diagnosticsEventSink.DiagnosticsHandler; + result.Diagnostics += this.EventSink.DiagnosticsHandler; + result.MinimumLevel = DiagnosticLevel.Verbose; return result; } @@ -72,5 +79,107 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers result.Seek(0); return result; } + + /// <summary> + /// Verifies the summary event generated by a processing run. + /// </summary> + /// <param name="configurationSet">The configuration set.</param> + /// <param name="setResult">The set result.</param> + /// <param name="resultSource">The result source.</param> + protected void VerifySummaryEvent(ConfigurationSet configurationSet, ApplyConfigurationSetResult setResult, ConfigurationUnitResultSource resultSource) + { + TelemetryEvent summary = this.VerifySummaryEventShared(configurationSet, ConfigurationUnitIntent.Apply, resultSource == ConfigurationUnitResultSource.None ? 0 : setResult.ResultCode.HResult, resultSource); + + int[] counts = new int[3]; + int[] runs = new int[3]; + int[] failures = new int[3]; + + foreach (ApplyConfigurationUnitResult unitResult in setResult.UnitResults) + { + SummaryCountByIntent(counts, runs, failures, unitResult.Unit.Intent, unitResult.ResultInformation); + } + + VerifySummaryCounts(summary, counts, runs, failures); + } + + /// <summary> + /// Verifies the summary event generated by a processing run. + /// </summary> + /// <param name="configurationSet">The configuration set.</param> + /// <param name="setResult">The set result.</param> + /// <param name="resultCode">The result code.</param> + /// <param name="resultSource">The result source.</param> + protected void VerifySummaryEvent(ConfigurationSet configurationSet, TestConfigurationSetResult setResult, int resultCode, ConfigurationUnitResultSource resultSource) + { + TelemetryEvent summary = this.VerifySummaryEventShared(configurationSet, ConfigurationUnitIntent.Assert, resultCode, resultSource); + + int[] counts = new int[3]; + int[] runs = new int[3]; + int[] failures = new int[3]; + + foreach (TestConfigurationUnitResult unitResult in setResult.UnitResults) + { + SummaryCountByIntent(counts, runs, failures, unitResult.Unit.Intent, unitResult.ResultInformation); + } + + VerifySummaryCounts(summary, counts, runs, failures); + } + + private static void SummaryCountByIntent(int[] counts, int[] runs, int[] failures, ConfigurationUnitIntent intent, ConfigurationUnitResultInformation resultInformation) + { + int index = (int)intent; + + counts[index]++; + + if (resultInformation.ResultSource != ConfigurationUnitResultSource.ConfigurationSet && resultInformation.ResultSource != ConfigurationUnitResultSource.Precondition) + { + runs[index]++; + } + + if (resultInformation.ResultCode != null) + { + failures[index]++; + } + } + + private static void VerifySummaryCounts(TelemetryEvent summary, int[] counts, int[] runs, int[] failures) + { + Assert.Equal(counts[(int)ConfigurationUnitIntent.Assert].ToString(), summary.Properties[TelemetryEvent.AssertCount]); + Assert.Equal(runs[(int)ConfigurationUnitIntent.Assert].ToString(), summary.Properties[TelemetryEvent.AssertsRun]); + Assert.Equal(failures[(int)ConfigurationUnitIntent.Assert].ToString(), summary.Properties[TelemetryEvent.AssertsFailed]); + + Assert.Equal(counts[(int)ConfigurationUnitIntent.Inform].ToString(), summary.Properties[TelemetryEvent.InformCount]); + Assert.Equal(runs[(int)ConfigurationUnitIntent.Inform].ToString(), summary.Properties[TelemetryEvent.InformsRun]); + Assert.Equal(failures[(int)ConfigurationUnitIntent.Inform].ToString(), summary.Properties[TelemetryEvent.InformsFailed]); + + Assert.Equal(counts[(int)ConfigurationUnitIntent.Apply].ToString(), summary.Properties[TelemetryEvent.ApplyCount]); + Assert.Equal(runs[(int)ConfigurationUnitIntent.Apply].ToString(), summary.Properties[TelemetryEvent.AppliesRun]); + Assert.Equal(failures[(int)ConfigurationUnitIntent.Apply].ToString(), summary.Properties[TelemetryEvent.AppliesFailed]); + } + + /// <summary> + /// Verifies the summary event generated by a processing run. + /// </summary> + /// <param name="configurationSet">The configuration set.</param> + /// <param name="runIntent">The run intent.</param> + /// <param name="resultCode">The result code.</param> + /// <param name="resultSource">The result source.</param> + private TelemetryEvent VerifySummaryEventShared(ConfigurationSet configurationSet, ConfigurationUnitIntent runIntent, int resultCode, ConfigurationUnitResultSource resultSource) + { + Assert.Single(this.EventSink.Events); + TelemetryEvent summary = this.EventSink.Events[0]; + + Assert.Equal(TelemetryEvent.ConfigProcessingSummaryName, summary.Name); + Assert.NotEqual(string.Empty, summary.CodeVersion); + Assert.NotEqual(Guid.Empty, summary.ActivityID); + Assert.Equal(string.Empty, summary.Caller); + Assert.Equal(configurationSet.InstanceIdentifier, Guid.Parse(summary.Properties[TelemetryEvent.SetID])); + Assert.False(int.Parse(summary.Properties[TelemetryEvent.FromHistory]) != 0); + Assert.Equal(((int)runIntent).ToString(), summary.Properties[TelemetryEvent.RunIntent]); + Assert.Equal(resultCode.ToString(), summary.Properties[TelemetryEvent.Result]); + Assert.Equal(((int)resultSource).ToString(), summary.Properties[TelemetryEvent.FailurePoint]); + + return summary; + } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/DiagnosticsEventSink.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/DiagnosticsEventSink.cs @@ -6,6 +6,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers { + using System.Collections.Generic; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Xunit.Abstractions; using Xunit.Sdk; @@ -13,7 +14,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers /// <summary> /// This class aids in getting diagnostics data from the <see cref="ConfigurationProcessor"/> out to the xUnit infrastructure. /// </summary> - internal class DiagnosticsEventSink + public class DiagnosticsEventSink { private readonly UnitTestFixture fixture; private readonly ITestOutputHelper log; @@ -30,12 +31,22 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers } /// <summary> + /// Gets the telemetry events that have been seen. + /// </summary> + public List<TelemetryEvent> Events { get; private set; } = new List<TelemetryEvent>(); + + /// <summary> /// Handles diagnostic information from a <see cref="ConfigurationProcessor"/>. /// </summary> /// <param name="sender">The object sending the information.</param> /// <param name="e">The diagnostic information.</param> public void DiagnosticsHandler(object? sender, DiagnosticInformation e) { + if (e.Message.Contains(TelemetryEvent.Preamble)) + { + this.Events.Add(new TelemetryEvent(e.Message)); + } + if (e.Level == DiagnosticLevel.Verbose) { this.fixture.MessageSink.OnMessage(new DiagnosticMessage(e.Message)); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TelemetryEvent.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TelemetryEvent.cs @@ -0,0 +1,140 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TelemetryEvent.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using System; + using System.Collections.Generic; + + /// <summary> + /// This class holds the data about a telemetry event detected via the diagnostics side channel. + /// </summary> + public class TelemetryEvent + { + /// <summary> + /// The initial indicator that the diagnostics message contains the contents of a telemetry event. + /// </summary> + public const string Preamble = "#DebugEventStream"; + + /// <summary> + /// The name of the ConfigUnitRun event. + /// </summary> + public const string ConfigUnitRunName = "ConfigUnitRun"; + + /// <summary> + /// The name of the ConfigProcessingSummary event. + /// </summary> + public const string ConfigProcessingSummaryName = "ConfigProcessingSummary"; + +#pragma warning disable SA1600 // Elements should be documented + + // Shared fields + public const string SetID = "SetID"; + public const string RunIntent = "RunIntent"; + public const string Result = "Result"; + public const string FailurePoint = "FailurePoint"; + + // ConfigUnitRun fields + public const string UnitID = "UnitID"; + public const string UnitName = "UnitName"; + public const string ModuleName = "ModuleName"; + public const string UnitIntent = "UnitIntent"; + public const string Action = "Action"; + public const string SettingsProvided = "SettingsProvided"; + + // ConfigProcessingSummary fields + public const string FromHistory = "FromHistory"; + public const string AssertCount = "AssertCount"; + public const string AssertsRun = "AssertsRun"; + public const string AssertsFailed = "AssertsFailed"; + public const string InformCount = "InformCount"; + public const string InformsRun = "InformsRun"; + public const string InformsFailed = "InformsFailed"; + public const string ApplyCount = "ApplyCount"; + public const string AppliesRun = "AppliesRun"; + public const string AppliesFailed = "AppliesFailed"; +#pragma warning restore SA1600 // Elements should be documented + + /// <summary> + /// Initializes a new instance of the <see cref="TelemetryEvent"/> class. + /// </summary> + /// <param name="eventMessage">The message containing the event data.</param> + public TelemetryEvent(string eventMessage) + { + bool preambleSeen = false; + + foreach (string line in eventMessage.Split('\n')) + { + if (line == Preamble) + { + preambleSeen = true; + continue; + } + + if (!preambleSeen) + { + // Skip all lines until the preamble is seen + continue; + } + + int splitIndex = line.IndexOf(": "); + if (splitIndex != -1) + { + this.Properties.Add(line.Substring(0, splitIndex), line.Substring(splitIndex + 2)); + } + } + } + + /// <summary> + /// Gets the properties for this event. + /// </summary> + public Dictionary<string, string> Properties { get; private set; } = new Dictionary<string, string>(); + + /// <summary> + /// Gets the name of the event. + /// </summary> + public string Name + { + get + { + return this.Properties["Event"]; + } + } + + /// <summary> + /// Gets the activity id. + /// </summary> + public Guid ActivityID + { + get + { + return Guid.Parse(this.Properties["ActivityID"]); + } + } + + /// <summary> + /// Gets the version of the code. + /// </summary> + public string CodeVersion + { + get + { + return this.Properties["CodeVersion"]; + } + } + + /// <summary> + /// Gets the caller. + /// </summary> + public string Caller + { + get + { + return this.Properties["Caller"]; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestConfigurationSetProcessor.cs @@ -95,5 +95,17 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers this.Processors[unit] = new TestConfigurationUnitProcessor(unit); return this.Processors[unit]; } + + /// <summary> + /// Creates a new unit processor details for the given unit. + /// </summary> + /// <param name="unit">The unit.</param> + /// <param name="detailLevel">The detail level requested.</param> + /// <returns>The details requested.</returns> + internal TestConfigurationUnitProcessorDetails CreateUnitDetails(ConfigurationUnit unit, ConfigurationUnitDetailLevel detailLevel) + { + this.Details[unit] = new TestConfigurationUnitProcessorDetails(unit, detailLevel); + return this.Details[unit]; + } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorApplyTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorApplyTests.cs @@ -13,6 +13,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests using System.Linq; using System.Runtime.InteropServices; using System.Threading; + using Microsoft.CodeAnalysis.Emit; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Microsoft.Management.Configuration.UnitTests.Helpers; using Microsoft.VisualBasic; @@ -50,6 +51,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); Assert.Throws<FileNotFoundException>(() => processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None)); + + Assert.Empty(this.EventSink.Events); } /// <summary> @@ -86,6 +89,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(unitResult.ResultInformation); Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.Equal(Errors.WINGET_CONFIG_ERROR_DUPLICATE_IDENTIFIER, unitResult.ResultInformation.ResultCode.HResult); + Assert.Equal(ConfigurationUnitResultSource.ConfigurationSet, unitResult.ResultInformation.ResultSource); } ApplyConfigurationUnitResult unitResultDifferentIdentifier = result.UnitResults.First(x => x.Unit == configurationUnitDifferentIdentifier); @@ -94,6 +98,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.False(unitResultDifferentIdentifier.RebootRequired); Assert.NotNull(unitResultDifferentIdentifier.ResultInformation); Assert.Null(unitResultDifferentIdentifier.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, unitResultDifferentIdentifier.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.ConfigurationSet); } /// <summary> @@ -125,6 +132,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.False(unitResult.RebootRequired); Assert.NotNull(unitResult.ResultInformation); Assert.Null(unitResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, unitResult.ResultInformation.ResultSource); unitResult = result.UnitResults.First(x => x.Unit == configurationUnitMissingDependency); Assert.NotNull(unitResult); @@ -133,6 +141,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(unitResult.ResultInformation); Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.Equal(Errors.WINGET_CONFIG_ERROR_MISSING_DEPENDENCY, unitResult.ResultInformation.ResultCode.HResult); + Assert.Equal(ConfigurationUnitResultSource.ConfigurationSet, unitResult.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.ConfigurationSet); } /// <summary> @@ -171,7 +182,10 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(unitResult.ResultInformation); Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.Equal(Errors.WINGET_CONFIG_ERROR_DEPENDENCY_UNSATISFIED, unitResult.ResultInformation.ResultCode.HResult); + Assert.Equal(ConfigurationUnitResultSource.Precondition, unitResult.ResultInformation.ResultSource); } + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.Precondition); } /// <summary> @@ -207,6 +221,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.False(unitResult.RebootRequired); Assert.NotNull(unitResult.ResultInformation); Assert.Null(unitResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, unitResult.ResultInformation.ResultSource); } Assert.Equal(1, unitProcessorAssert.TestSettingsCalls); @@ -220,6 +235,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.Equal(1, unitProcessorApply.TestSettingsCalls); Assert.Equal(0, unitProcessorApply.GetSettingsCalls); Assert.Equal(1, unitProcessorApply.ApplySettingsCalls); + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.None); } /// <summary> @@ -255,6 +272,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(unitResult.ResultInformation); Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.IsType<NullReferenceException>(unitResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.Internal, unitResult.ResultInformation.ResultSource); unitResult = result.UnitResults.First(x => x.Unit == configurationUnitApply); Assert.NotNull(unitResult); @@ -263,6 +281,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(unitResult.ResultInformation); Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.Equal(Errors.WINGET_CONFIG_ERROR_ASSERTION_FAILED, unitResult.ResultInformation.ResultCode.HResult); + Assert.Equal(ConfigurationUnitResultSource.Precondition, unitResult.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.Internal); } /// <summary> @@ -299,7 +320,10 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(unitResult.ResultInformation); Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.Equal(Errors.WINGET_CONFIG_ERROR_ASSERTION_FAILED, unitResult.ResultInformation.ResultCode.HResult); + Assert.Equal(ConfigurationUnitResultSource.Precondition, unitResult.ResultInformation.ResultSource); } + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.Precondition); } /// <summary> @@ -330,6 +354,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.False(unitResult.RebootRequired); Assert.NotNull(unitResult.ResultInformation); Assert.Null(unitResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, unitResult.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.None); } /// <summary> @@ -412,11 +439,13 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests if (expectedProgress[i].HResult == 0) { Assert.Null(progressEvents[i].ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, progressEvents[i].ResultInformation.ResultSource); } else { Assert.NotNull(progressEvents[i].ResultInformation.ResultCode); Assert.Equal(expectedProgress[i].HResult, progressEvents[i].ResultInformation.ResultCode.HResult); + Assert.Equal(ConfigurationUnitResultSource.Precondition, progressEvents[i].ResultInformation.ResultSource); } break; @@ -425,6 +454,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests break; } } + + this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.Precondition); } private struct ExpectedConfigurationChangeData diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorTelemetryTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorTelemetryTests.cs @@ -0,0 +1,236 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationProcessorTelemetryTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Runtime.InteropServices; + using System.Threading; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Microsoft.VisualBasic; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + using Xunit; + using Xunit.Abstractions; + using static System.Collections.Specialized.BitVector32; + + /// <summary> + /// Unit tests for running test on the processor. + /// </summary> + [Collection("UnitTestCollection")] + public class ConfigurationProcessorTelemetryTests : ConfigurationProcessorTestBase + { + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationProcessorTelemetryTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ConfigurationProcessorTelemetryTests(UnitTestFixture fixture, ITestOutputHelper log) + : base(fixture, log) + { + } + + /// <summary> + /// No event is generated if the unit succeeds. + /// </summary> + [Fact] + public void Telemetry_NoUnitEventOnSuccess() + { + TelemetryTestObjects testObjects = new TelemetryTestObjects(getFails: false); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + testObjects.CreateDetails(); + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + Assert.Empty(this.EventSink.Events); + } + + /// <summary> + /// No event is generated if the details have not been retrieved. + /// </summary> + [Fact] + public void Telemetry_NoUnitEventIfNoDetails() + { + TelemetryTestObjects testObjects = new TelemetryTestObjects(); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + Assert.Empty(this.EventSink.Events); + } + + /// <summary> + /// No event is generated if the module is not public. + /// </summary> + [Fact] + public void Telemetry_NoUnitEventIfNotPublic() + { + TelemetryTestObjects testObjects = new TelemetryTestObjects(); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + testObjects.CreateDetails(isPublic: false); + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + Assert.Empty(this.EventSink.Events); + } + + /// <summary> + /// The activity set by the caller is the value in the event. + /// </summary> + [Fact] + public void Telemetry_ActivityID() + { + TelemetryTestObjects testObjects = new TelemetryTestObjects(); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + testObjects.CreateDetails(); + + Guid activity = Guid.NewGuid(); + testObjects.Processor.ActivityIdentifier = activity; + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + Assert.Single(this.EventSink.Events); + Assert.Equal(TelemetryEvent.ConfigUnitRunName, this.EventSink.Events[0].Name); + Assert.Equal(activity, this.EventSink.Events[0].ActivityID); + } + + /// <summary> + /// Disabling telemetry causes no event to be produced. + /// </summary> + /// <param name="state">The state of telemetry.</param> + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Telemetry_EnableState(bool state) + { + TelemetryTestObjects testObjects = new TelemetryTestObjects(); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + testObjects.CreateDetails(); + + testObjects.Processor.GenerateTelemetryEvents = state; + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + if (state) + { + Assert.Single(this.EventSink.Events); + Assert.Equal(TelemetryEvent.ConfigUnitRunName, this.EventSink.Events[0].Name); + } + else + { + Assert.Empty(this.EventSink.Events); + } + } + + /// <summary> + /// The caller set by the caller is the value in the event. + /// </summary> + [Fact] + public void Telemetry_Caller() + { + TelemetryTestObjects testObjects = new TelemetryTestObjects(); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + testObjects.CreateDetails(); + + string caller = "TheTests"; + testObjects.Processor.Caller = caller; + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + Assert.Single(this.EventSink.Events); + Assert.Equal(TelemetryEvent.ConfigUnitRunName, this.EventSink.Events[0].Name); + Assert.Equal(caller, this.EventSink.Events[0].Caller); + } + + /// <summary> + /// Verifies all of the telemetry fields that come from executing a specific unit. + /// </summary> + [Fact] + public void Telemetry_UnitFields() + { +#pragma warning disable CS8602 // Dereference of a possibly null reference. + TelemetryTestObjects testObjects = new TelemetryTestObjects(); + testObjects.Processor = this.CreateConfigurationProcessorWithDiagnostics(testObjects.Factory); + testObjects.CreateDetails(); + + testObjects.Unit.UnitName = "TestUnitName"; + testObjects.UnitDetails.ModuleName = "TestModuleName"; + + string setting1 = "setting1"; + string setting2 = "setting2"; + + testObjects.Unit.Settings.Add(setting1, 0); + testObjects.Unit.Settings.Add(setting2, 0); + + GetConfigurationUnitSettingsResult result = testObjects.Processor.GetUnitSettings(testObjects.Unit); + + Assert.Single(this.EventSink.Events); + TelemetryEvent runEvent = this.EventSink.Events[0]; + Assert.Equal(TelemetryEvent.ConfigUnitRunName, runEvent.Name); + Assert.NotEqual(string.Empty, runEvent.CodeVersion); + Assert.NotEqual(Guid.Empty, runEvent.ActivityID); + Assert.Equal(string.Empty, runEvent.Caller); + Assert.Equal(Guid.Empty, Guid.Parse(runEvent.Properties[TelemetryEvent.SetID])); + Assert.NotEqual(Guid.Empty, Guid.Parse(runEvent.Properties[TelemetryEvent.UnitID])); + Assert.Equal(testObjects.Unit.UnitName, runEvent.Properties[TelemetryEvent.UnitName]); + Assert.Equal(testObjects.UnitDetails.ModuleName, runEvent.Properties[TelemetryEvent.ModuleName]); + Assert.Equal(((int)testObjects.Unit.Intent).ToString(), runEvent.Properties[TelemetryEvent.UnitIntent]); + Assert.Equal(((int)ConfigurationUnitIntent.Inform).ToString(), runEvent.Properties[TelemetryEvent.RunIntent]); + Assert.NotEqual(string.Empty, runEvent.Properties[TelemetryEvent.Action]); + Assert.Equal(testObjects.GetResult.ResultInformation.ResultCode.HResult.ToString(), runEvent.Properties[TelemetryEvent.Result]); + Assert.Equal(((int)testObjects.GetResult.ResultInformation.ResultSource).ToString(), runEvent.Properties[TelemetryEvent.FailurePoint]); + Assert.Equal(setting1 + "|" + setting2, runEvent.Properties[TelemetryEvent.SettingsProvided]); +#pragma warning restore CS8602 // Dereference of a possibly null reference. + } + + private class TelemetryTestObjects + { + public TelemetryTestObjects(bool getFails = true) + { + this.Unit = new ConfigurationUnit { Intent = ConfigurationUnitIntent.Apply }; + + this.Factory = new TestConfigurationProcessorFactory(); + this.Factory.NullProcessor = new TestConfigurationSetProcessor(null); + + this.UnitProcessor = this.Factory.NullProcessor.CreateTestProcessor(this.Unit); + + if (getFails) + { + this.GetResult = new GetSettingsResult(); + this.GetResult.ResultInformation.ResultCode = new NullReferenceException(); + this.GetResult.ResultInformation.ResultSource = ConfigurationUnitResultSource.UnitProcessing; + this.UnitProcessor.GetSettingsDelegate = () => this.GetResult; + } + } + + public ConfigurationUnit Unit { get; set; } + + public TestConfigurationProcessorFactory Factory { get; set; } + + public TestConfigurationUnitProcessor UnitProcessor { get; set; } + + public GetSettingsResult? GetResult { get; set; } + + public TestConfigurationUnitProcessorDetails? UnitDetails { get; set; } + + public ConfigurationProcessor? Processor { get; set; } + + public void CreateDetails(bool isPublic = true) + { +#pragma warning disable CS8602 // Dereference of a possibly null reference. + this.UnitDetails = this.Factory.NullProcessor.CreateUnitDetails(this.Unit, ConfigurationUnitDetailLevel.Catalog); +#pragma warning restore CS8602 // Dereference of a possibly null reference. + this.UnitDetails.IsPublic = isPublic; + + this.Processor?.GetUnitDetails(this.Unit, ConfigurationUnitDetailLevel.Catalog); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorTestTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorTestTests.cs @@ -12,6 +12,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests using System.IO; using System.Linq; using System.Runtime.InteropServices; + using Microsoft.CodeAnalysis.Emit; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Microsoft.Management.Configuration.UnitTests.Helpers; using Microsoft.VisualBasic; @@ -49,6 +50,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); Assert.Throws<FileNotFoundException>(() => processor.TestSet(configurationSet)); + + Assert.Empty(this.EventSink.Events); } /// <summary> @@ -81,12 +84,16 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(throwsResult.ResultInformation); Assert.NotNull(throwsResult.ResultInformation.ResultCode); Assert.IsType<NullReferenceException>(throwsResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.Internal, throwsResult.ResultInformation.ResultSource); TestConfigurationUnitResult worksResult = result.UnitResults.First(x => x.Unit == configurationUnitWorks); Assert.NotNull(worksResult); Assert.Equal(ConfigurationTestResult.Positive, worksResult.TestResult); Assert.NotNull(worksResult.ResultInformation); Assert.Null(worksResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, worksResult.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, throwsResult.ResultInformation.ResultCode.HResult, ConfigurationUnitResultSource.Internal); } /// <summary> @@ -120,12 +127,16 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(throwsResult.ResultInformation); Assert.NotNull(throwsResult.ResultInformation.ResultCode); Assert.IsType<NullReferenceException>(throwsResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.Internal, throwsResult.ResultInformation.ResultSource); TestConfigurationUnitResult worksResult = result.UnitResults.First(x => x.Unit == configurationUnitWorks); Assert.NotNull(worksResult); Assert.Equal(ConfigurationTestResult.Positive, worksResult.TestResult); Assert.NotNull(worksResult.ResultInformation); Assert.Null(worksResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, worksResult.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, throwsResult.ResultInformation.ResultCode.HResult, ConfigurationUnitResultSource.Internal); } /// <summary> @@ -146,6 +157,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests testResult.TestResult = ConfigurationTestResult.Failed; testResult.ResultInformation.ResultCode = new NullReferenceException(); testResult.ResultInformation.Description = "Failed again"; + testResult.ResultInformation.ResultSource = ConfigurationUnitResultSource.UnitProcessing; unitProcessor.TestSettingsDelegate = () => testResult; ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); @@ -164,12 +176,16 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(throwsResult.ResultInformation.ResultCode); Assert.IsType<NullReferenceException>(throwsResult.ResultInformation.ResultCode); Assert.Equal(testResult.ResultInformation.Description, throwsResult.ResultInformation.Description); + Assert.Equal(testResult.ResultInformation.ResultSource, throwsResult.ResultInformation.ResultSource); TestConfigurationUnitResult worksResult = result.UnitResults.First(x => x.Unit == configurationUnitWorks); Assert.NotNull(worksResult); Assert.Equal(ConfigurationTestResult.Positive, worksResult.TestResult); Assert.NotNull(worksResult.ResultInformation); Assert.Null(worksResult.ResultInformation.ResultCode); + Assert.Equal(ConfigurationUnitResultSource.None, worksResult.ResultInformation.ResultSource); + + this.VerifySummaryEvent(configurationSet, result, testResult.ResultInformation.ResultCode.HResult, testResult.ResultInformation.ResultSource); } /// <summary> @@ -222,6 +238,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests failedResult.TestResult = ConfigurationTestResult.Failed; failedResult.ResultInformation.ResultCode = new NullReferenceException(); failedResult.ResultInformation.Description = "Failed again"; + failedResult.ResultInformation.ResultSource = ConfigurationUnitResultSource.UnitProcessing; for (int i = 0; i < resultTypes.Length; ++i) { @@ -257,6 +274,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.NotNull(result.UnitResults); Assert.Equal(resultTypes.Length, result.UnitResults.Count); + int summaryEventResult = 0; + ConfigurationUnitResultSource resultSource = ConfigurationUnitResultSource.None; + for (int i = 0; i < resultTypes.Length; ++i) { TestConfigurationUnitResult unitResult = result.UnitResults.First(x => x.Unit == configurationUnits[i]); @@ -272,14 +292,20 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests case ConfigurationTestResult.NotRun: Assert.Null(unitResult.ResultInformation.ResultCode); Assert.Empty(unitResult.ResultInformation.Description); + Assert.Equal(ConfigurationUnitResultSource.None, unitResult.ResultInformation.ResultSource); break; case ConfigurationTestResult.Failed: Assert.NotNull(unitResult.ResultInformation.ResultCode); Assert.IsType<NullReferenceException>(unitResult.ResultInformation.ResultCode); Assert.Equal(failedResult.ResultInformation.Description, unitResult.ResultInformation.Description); + Assert.Equal(failedResult.ResultInformation.ResultSource, unitResult.ResultInformation.ResultSource); + summaryEventResult = unitResult.ResultInformation.ResultCode.HResult; + resultSource = unitResult.ResultInformation.ResultSource; break; } } + + this.VerifySummaryEvent(configurationSet, result, summaryEventResult, resultSource); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs @@ -106,6 +106,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // Do not check for the type. Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + Assert.Equal(ConfigurationUnitResultSource.Internal, result.ResultInformation.ResultSource); } /// <summary> @@ -134,29 +135,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // Do not check for the type. Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); - } - - /// <summary> - /// Tests GetSettings when a non specialized catched exception is thrown. - /// </summary> - [Fact] - public void GetSettings_Throws() - { - var processorEnvMock = new Mock<IProcessorEnvironment>(); - processorEnvMock.Setup(m => m.InvokeGetResource( - It.IsAny<ValueSet>(), - It.IsAny<string>(), - It.IsAny<ModuleSpecification?>())) - .Throws(() => new ArgumentNullException("a message")) - .Verifiable(); - - var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Inform); - - var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); - - Assert.Throws<ArgumentNullException>(() => unitProcessor.GetSettings()); - - processorEnvMock.Verify(); + Assert.Equal(ConfigurationUnitResultSource.Internal, result.ResultInformation.ResultSource); } /// <summary> @@ -238,6 +217,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // Do not check for the type. Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + Assert.Equal(ConfigurationUnitResultSource.Internal, result.ResultInformation.ResultSource); } /// <summary> @@ -268,29 +248,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // Do not check for the type. Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); - } - - /// <summary> - /// Tests TestSettings when a non specialized catched exception is thrown. - /// </summary> - [Fact] - public void TestSettings_Throws() - { - var processorEnvMock = new Mock<IProcessorEnvironment>(); - processorEnvMock.Setup(m => m.InvokeTestResource( - It.IsAny<ValueSet>(), - It.IsAny<string>(), - It.IsAny<ModuleSpecification?>())) - .Throws(() => new ArgumentNullException("a message")) - .Verifiable(); - - var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Assert); - - var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); - - Assert.Throws<ArgumentNullException>(() => unitProcessor.TestSettings()); - - processorEnvMock.Verify(); + Assert.Equal(ConfigurationUnitResultSource.Internal, result.ResultInformation.ResultSource); } /// <summary> @@ -362,6 +320,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // Do not check for the type. Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + Assert.Equal(ConfigurationUnitResultSource.Internal, result.ResultInformation.ResultSource); } /// <summary> @@ -390,29 +349,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests // Do not check for the type. Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); - } - - /// <summary> - /// Tests ApplySettings when a non specialized catched exception is thrown. - /// </summary> - [Fact] - public void ApplySettings_Throws() - { - var processorEnvMock = new Mock<IProcessorEnvironment>(); - processorEnvMock.Setup(m => m.InvokeSetResource( - It.IsAny<ValueSet>(), - It.IsAny<string>(), - It.IsAny<ModuleSpecification?>())) - .Throws(() => new ArgumentNullException("a message")) - .Verifiable(); - - var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Apply); - - var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); - - Assert.Throws<ArgumentNullException>(() => unitProcessor.ApplySettings()); - - processorEnvMock.Verify(); + Assert.Equal(ConfigurationUnitResultSource.Internal, result.ResultInformation.ResultSource); } private ConfigurationUnitAndResource CreateUnitResource(ConfigurationUnitIntent intent) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DscModuleV2Tests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DscModuleV2Tests.cs @@ -279,7 +279,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests var dscModule = new DscModuleV2(); using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var exception = Assert.Throws<RuntimeException>(() => + var exception = Assert.Throws<InvokeDscResourceException>(() => dscModule.InvokeGetResource( pwsh, new ValueSet(), @@ -319,7 +319,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests var dscModule = new DscModuleV2(); using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var exception = Assert.Throws<RuntimeException>( + var exception = Assert.Throws<InvokeDscResourceException>( () => dscModule.InvokeGetResource( pwsh, new ValueSet(), @@ -327,7 +327,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests PowerShellHelpers.CreateModuleSpecification( TestModule.SimpleTestResourceModuleName))); - Assert.IsType<WriteErrorException>(exception.InnerException); + Assert.IsType<RuntimeException>(exception.InnerException); } /// <summary> @@ -370,7 +370,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests var dscModule = new DscModuleV2(); using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var exception = Assert.Throws<RuntimeException>(() => + var exception = Assert.Throws<InvokeDscResourceException>(() => dscModule.InvokeTestResource( pwsh, new ValueSet(), @@ -410,7 +410,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests var dscModule = new DscModuleV2(); using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var exception = Assert.Throws<RuntimeException>(() => + var exception = Assert.Throws<InvokeDscResourceException>(() => _ = dscModule.InvokeTestResource( pwsh, new ValueSet(), @@ -418,7 +418,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests PowerShellHelpers.CreateModuleSpecification( TestModule.SimpleTestResourceModuleName))); - Assert.IsType<WriteErrorException>(exception.InnerException); + Assert.IsType<RuntimeException>(exception.InnerException); } /// <summary> @@ -460,7 +460,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests var dscModule = new DscModuleV2(); using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var exception = Assert.Throws<RuntimeException>(() => + var exception = Assert.Throws<InvokeDscResourceException>(() => dscModule.InvokeSetResource( pwsh, new ValueSet(), @@ -500,7 +500,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests var dscModule = new DscModuleV2(); using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var exception = Assert.Throws<RuntimeException>(() => + var exception = Assert.Throws<InvokeDscResourceException>(() => dscModule.InvokeSetResource( pwsh, new ValueSet(), @@ -508,7 +508,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests PowerShellHelpers.CreateModuleSpecification( TestModule.SimpleTestResourceModuleName))); - Assert.IsType<WriteErrorException>(exception.InnerException); + Assert.IsType<RuntimeException>(exception.InnerException); } /// <summary> @@ -560,8 +560,6 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests /// <summary> /// Calls Invoke-DscResource invalid arguments. /// </summary> - /// <param name="value">Setting value.</param> - /// <param name="rebootRequired">Expected reboot required.</param> [Fact] public void InvokeSetResource_InvalidArguments() { @@ -575,7 +573,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests }; using PowerShell pwsh = PowerShell.Create(testEnvironment.Runspace); - var e = Assert.Throws<InvokeDscResourceSetException>(() => dscModule.InvokeSetResource( + var e = Assert.Throws<InvokeDscResourceException>(() => dscModule.InvokeSetResource( pwsh, settings, TestModule.SimpleTestResourceName, @@ -583,6 +581,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests TestModule.SimpleTestResourceModuleName))); Assert.Contains("The property 'Fake' cannot be found on this object.", e.Message); + Assert.Equal(ConfigurationUnitResultSource.ConfigurationSet, e.ResultSource); } } } diff --git a/src/Microsoft.Management.Configuration/ApplyConfigurationSetResult.cpp b/src/Microsoft.Management.Configuration/ApplyConfigurationSetResult.cpp @@ -10,7 +10,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_unitResults(single_threaded_vector<ApplyConfigurationUnitResult>()) {} - Windows::Foundation::Collections::IVectorView<ApplyConfigurationUnitResult> ApplyConfigurationSetResult::UnitResults() + Windows::Foundation::Collections::IVectorView<ApplyConfigurationUnitResult> ApplyConfigurationSetResult::UnitResults() const { return m_unitResults.GetView(); } @@ -20,7 +20,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation return m_unitResults; } - hresult ApplyConfigurationSetResult::ResultCode() + hresult ApplyConfigurationSetResult::ResultCode() const { return m_resultCode; } diff --git a/src/Microsoft.Management.Configuration/ApplyConfigurationSetResult.h b/src/Microsoft.Management.Configuration/ApplyConfigurationSetResult.h @@ -17,8 +17,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ResultCode(hresult value); #endif - Windows::Foundation::Collections::IVectorView<ApplyConfigurationUnitResult> UnitResults(); - hresult ResultCode(); + Windows::Foundation::Collections::IVectorView<ApplyConfigurationUnitResult> UnitResults() const; + hresult ResultCode() const; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: diff --git a/src/Microsoft.Management.Configuration/ConfigThreadGlobals.cpp b/src/Microsoft.Management.Configuration/ConfigThreadGlobals.cpp @@ -10,8 +10,18 @@ namespace winrt::Microsoft::Management::Configuration::implementation return m_logger; } - AppInstaller::Logging::TelemetryTraceLogger& ConfigThreadGlobals::GetTelemetryLogger() + void* ConfigThreadGlobals::GetTelemetryObject() { - THROW_HR(E_NOTIMPL); + return &m_telemetry; + } + + TelemetryTraceLogger& ConfigThreadGlobals::GetTelemetryLogger() + { + return m_telemetry; + } + + const TelemetryTraceLogger& ConfigThreadGlobals::GetTelemetryLogger() const + { + return m_telemetry; } } diff --git a/src/Microsoft.Management.Configuration/ConfigThreadGlobals.h b/src/Microsoft.Management.Configuration/ConfigThreadGlobals.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include <winget/SharedThreadGlobals.h> +#include <Telemetry/Telemetry.h> namespace winrt::Microsoft::Management::Configuration::implementation { @@ -13,9 +14,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation AppInstaller::Logging::DiagnosticLogger& GetDiagnosticLogger() override; - AppInstaller::Logging::TelemetryTraceLogger& GetTelemetryLogger() override; + void* GetTelemetryObject() override; + + TelemetryTraceLogger& GetTelemetryLogger(); + const TelemetryTraceLogger& GetTelemetryLogger() const; protected: AppInstaller::Logging::DiagnosticLogger m_logger; + TelemetryTraceLogger m_telemetry; }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -25,6 +25,32 @@ namespace winrt::Microsoft::Management::Configuration::implementation { namespace { + AppInstaller::Logging::Level ConvertLevel(DiagnosticLevel level) + { + switch (level) + { + case DiagnosticLevel::Verbose: return AppInstaller::Logging::Level::Verbose; + case DiagnosticLevel::Informational: return AppInstaller::Logging::Level::Info; + case DiagnosticLevel::Warning: return AppInstaller::Logging::Level::Warning; + case DiagnosticLevel::Error: return AppInstaller::Logging::Level::Error; + case DiagnosticLevel::Critical: return AppInstaller::Logging::Level::Crit; + default: return AppInstaller::Logging::Level::Warning; + } + } + + DiagnosticLevel ConvertLevel(AppInstaller::Logging::Level level) + { + switch (level) + { + case AppInstaller::Logging::Level::Verbose: return DiagnosticLevel::Verbose; + case AppInstaller::Logging::Level::Info: return DiagnosticLevel::Informational; + case AppInstaller::Logging::Level::Warning: return DiagnosticLevel::Warning; + case AppInstaller::Logging::Level::Error: return DiagnosticLevel::Error; + case AppInstaller::Logging::Level::Crit: return DiagnosticLevel::Critical; + default: return DiagnosticLevel::Warning; + } + } + // ILogger that sends data back to the Diagnostics event of the ConfigurationProcessor. struct ConfigurationProcessorDiagnosticsLogger : public AppInstaller::Logging::ILogger { @@ -43,26 +69,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation } catch (...) {} - void WriteDirect(std::string_view message) noexcept override try + void WriteDirect(AppInstaller::Logging::Channel, AppInstaller::Logging::Level level, std::string_view message) noexcept override try { - m_processor.Diagnostics(DiagnosticLevel::Informational, message); + m_processor.Diagnostics(ConvertLevel(level), message); } catch (...) {} private: - DiagnosticLevel ConvertLevel(AppInstaller::Logging::Level level) - { - switch (level) - { - case AppInstaller::Logging::Level::Verbose: return DiagnosticLevel::Verbose; - case AppInstaller::Logging::Level::Info: return DiagnosticLevel::Informational; - case AppInstaller::Logging::Level::Warning: return DiagnosticLevel::Warning; - case AppInstaller::Logging::Level::Error: return DiagnosticLevel::Error; - case AppInstaller::Logging::Level::Crit: return DiagnosticLevel::Critical; - default: return DiagnosticLevel::Warning; - } - } - ConfigurationProcessor& m_processor; }; @@ -129,7 +142,41 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationProcessor::MinimumLevel(DiagnosticLevel value) { m_minimumLevel = value; - m_factory.MinimumLevel(value); + m_threadGlobals.GetDiagnosticLogger().SetLevel(ConvertLevel(value)); + if (m_factory) + { + m_factory.MinimumLevel(value); + } + } + + hstring ConfigurationProcessor::Caller() const + { + return hstring{ AppInstaller::Utility::ConvertToUTF16(m_threadGlobals.GetTelemetryLogger().GetCaller()) }; + } + + void ConfigurationProcessor::Caller(hstring value) + { + m_threadGlobals.GetTelemetryLogger().SetCaller(AppInstaller::Utility::ConvertToUTF8(value)); + } + + guid ConfigurationProcessor::ActivityIdentifier() + { + return *m_threadGlobals.GetTelemetryLogger().GetActivityId(); + } + + void ConfigurationProcessor::ActivityIdentifier(const guid& value) + { + m_threadGlobals.GetTelemetryLogger().SetActivityId(value); + } + + bool ConfigurationProcessor::GenerateTelemetryEvents() + { + return m_threadGlobals.GetTelemetryLogger().IsEnabled(); + } + + void ConfigurationProcessor::GenerateTelemetryEvents(bool value) + { + std::ignore = m_threadGlobals.GetTelemetryLogger().EnableRuntime(value); } event_token ConfigurationProcessor::ConfigurationChange(const Windows::Foundation::TypedEventHandler<ConfigurationSet, ConfigurationChangeData>& handler) @@ -251,14 +298,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation IConfigurationUnitProcessorDetails details = setProcessor.GetUnitProcessorDetails(unit, detailLevel); get_self<implementation::ConfigurationUnit>(unit)->Details(std::move(details)); } - catch (const winrt::hresult_error& hre) - { - unitResultInformation->ResultCode(LOG_CAUGHT_EXCEPTION()); - unitResultInformation->Description(hre.message()); - } catch (...) { - unitResultInformation->ResultCode(LOG_CAUGHT_EXCEPTION()); + ExtractUnitResultInformation(std::current_exception(), unitResultInformation); } result->UnitResultsVector().Append(*unitResult); @@ -306,7 +348,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation auto threadGlobals = m_threadGlobals.SetForCurrentThread(); auto result = make_self<wil::details::module_count_wrapper<implementation::ApplyConfigurationSetResult>>(); - ConfigurationSetApplyProcessor applyProcessor{ localSet, m_factory.CreateSetProcessor(localSet), result, progress }; + ConfigurationSetApplyProcessor applyProcessor{ localSet, m_threadGlobals.GetTelemetryLogger(), m_factory.CreateSetProcessor(localSet), result, progress}; progress.set_result(*result); applyProcessor.Process(); @@ -368,6 +410,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation { ExtractUnitResultInformation(std::current_exception(), unitResult); } + + m_threadGlobals.GetTelemetryLogger().LogConfigUnitRunIfAppropriate(localSet.InstanceIdentifier(), unit, ConfigurationUnitIntent::Assert, TelemetryTraceLogger::TestAction, testResult->ResultInformation()); } } else @@ -385,6 +429,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation progress(*testResult); } + m_threadGlobals.GetTelemetryLogger().LogConfigProcessingSummaryForTest(*winrt::get_self<implementation::ConfigurationSet>(localSet), *result); co_return *result; } @@ -431,6 +476,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation { ExtractUnitResultInformation(std::current_exception(), unitResult); } + + m_threadGlobals.GetTelemetryLogger().LogConfigUnitRunIfAppropriate(GUID_NULL, localUnit, ConfigurationUnitIntent::Inform, TelemetryTraceLogger::GetAction, result->ResultInformation()); } co_return *result; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -33,6 +33,15 @@ namespace winrt::Microsoft::Management::Configuration::implementation DiagnosticLevel MinimumLevel(); void MinimumLevel(DiagnosticLevel value); + hstring Caller() const; + void Caller(hstring value); + + guid ActivityIdentifier(); + void ActivityIdentifier(const guid& value); + + bool GenerateTelemetryEvents(); + void GenerateTelemetryEvents(bool value); + event_token ConfigurationChange(const Windows::Foundation::TypedEventHandler<ConfigurationSet, ConfigurationChangeData>& handler); void ConfigurationChange(const event_token& token) noexcept; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSet.cpp b/src/Microsoft.Management.Configuration/ConfigurationSet.cpp @@ -23,6 +23,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_configurationUnits = winrt::single_threaded_vector<Configuration::ConfigurationUnit>(std::move(units)); } + bool ConfigurationSet::IsFromHistory() const + { + return false; + } + hstring ConfigurationSet::Name() { return m_name; @@ -56,7 +61,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_path = value; } - guid ConfigurationSet::InstanceIdentifier() + guid ConfigurationSet::InstanceIdentifier() const { return m_instanceIdentifier; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSet.h b/src/Microsoft.Management.Configuration/ConfigurationSet.h @@ -19,6 +19,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) ConfigurationSet(const guid& instanceIdentifier); void Initialize(std::vector<Configuration::ConfigurationUnit>&& units); + + bool IsFromHistory() const; #endif hstring Name(); @@ -30,7 +32,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring Path(); void Path(const hstring& value); - guid InstanceIdentifier(); + guid InstanceIdentifier() const; ConfigurationSetState State(); clock::time_point FirstApply(); clock::time_point ApplyBegun(); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetApplyProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetApplyProcessor.cpp @@ -20,8 +20,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } - ConfigurationSetApplyProcessor::ConfigurationSetApplyProcessor(const Configuration::ConfigurationSet& configurationSet, IConfigurationSetProcessor&& setProcessor, result_type result, const std::function<void(ConfigurationSetChangeData)>& progress) : - m_setProcessor(std::move(setProcessor)), m_result(std::move(result)), m_progress(progress) + ConfigurationSetApplyProcessor::ConfigurationSetApplyProcessor( + const Configuration::ConfigurationSet& configurationSet, + const TelemetryTraceLogger& telemetry, + IConfigurationSetProcessor&& setProcessor, + result_type result, + const std::function<void(ConfigurationSetChangeData)>& progress) : + m_configurationSet(configurationSet), m_setProcessor(std::move(setProcessor)), m_telemetry(telemetry), m_result(std::move(result)), m_progress(progress) { // Create a copy of the set of configuration units auto unitsView = configurationSet.ConfigurationUnits(); @@ -38,17 +43,17 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSetApplyProcessor::Process() { - if (!PreProcess()) + if (PreProcess()) { - return; - } + // TODO: When cross process is implemented, send Pending until we actually start + SendProgress(ConfigurationSetState::InProgress); - // TODO: When cross process is implemented, send Pending until we actually start - SendProgress(ConfigurationSetState::InProgress); + ProcessInternal(HasProcessedSuccessfully, &ConfigurationSetApplyProcessor::ProcessUnit, true); - ProcessInternal(HasProcessedSuccessfully, &ConfigurationSetApplyProcessor::ProcessUnit, true); + SendProgress(ConfigurationSetState::Completed); + } - SendProgress(ConfigurationSetState::Completed); + m_telemetry.LogConfigProcessingSummaryForApply(*winrt::get_self<implementation::ConfigurationSet>(m_configurationSet), *m_result); } ConfigurationSetApplyProcessor::UnitInfo::UnitInfo(const Configuration::ConfigurationUnit& unit) : @@ -93,7 +98,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (itr == m_idToUnitInfoIndex.end()) { AICLI_LOG(Config, Error, << "Found missing dependency: " << dependency); - unitInfo.ResultInformation->ResultCode(WINGET_CONFIG_ERROR_MISSING_DEPENDENCY); + unitInfo.ResultInformation->Initialize(WINGET_CONFIG_ERROR_MISSING_DEPENDENCY, ConfigurationUnitResultSource::ConfigurationSet); result = false; } else @@ -128,8 +133,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation { AICLI_LOG(Config, Error, << "Found duplicate identifier: " << identifier); // Found a duplicate identifier, mark both as such - unitInfo.ResultInformation->ResultCode(WINGET_CONFIG_ERROR_DUPLICATE_IDENTIFIER); - m_unitInfo[itr->second].ResultInformation->ResultCode(WINGET_CONFIG_ERROR_DUPLICATE_IDENTIFIER); + unitInfo.ResultInformation->Initialize(WINGET_CONFIG_ERROR_DUPLICATE_IDENTIFIER, ConfigurationUnitResultSource::ConfigurationSet); + m_unitInfo[itr->second].ResultInformation->Initialize(WINGET_CONFIG_ERROR_DUPLICATE_IDENTIFIER, ConfigurationUnitResultSource::ConfigurationSet); return false; } else @@ -224,7 +229,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (unitInfo.Unit.Intent() == intent) { hasRemainingDependencies = true; - unitInfo.ResultInformation->ResultCode(WINGET_CONFIG_ERROR_DEPENDENCY_UNSATISFIED); + unitInfo.ResultInformation->Initialize(WINGET_CONFIG_ERROR_DEPENDENCY_UNSATISFIED, ConfigurationUnitResultSource::Precondition); if (sendProgress) { SendProgress(ConfigurationUnitState::Skipped, unitInfo); @@ -240,7 +245,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation UnitInfo& unitInfo = m_unitInfo[index]; if (unitInfo.Unit.Intent() != intent) { - unitInfo.ResultInformation->ResultCode(errorForOtherIntents); + unitInfo.ResultInformation->Initialize(errorForOtherIntents, ConfigurationUnitResultSource::Precondition); if (sendProgress) { SendProgress(ConfigurationUnitState::Skipped, unitInfo); @@ -312,7 +317,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { // If the unit is requested to be skipped, we mark it with a failure to prevent any dependency from running. // But we return true from this function to indicate a successful "processing". - unitInfo.ResultInformation->ResultCode(WINGET_CONFIG_ERROR_MANUALLY_SKIPPED); + unitInfo.ResultInformation->Initialize(WINGET_CONFIG_ERROR_MANUALLY_SKIPPED, ConfigurationUnitResultSource::Precondition); SendProgress(ConfigurationUnitState::Skipped, unitInfo); return true; } @@ -331,92 +336,100 @@ namespace winrt::Microsoft::Management::Configuration::implementation return false; } + bool result = false; + std::string_view action; + try { switch (unitInfo.Unit.Intent()) { case ConfigurationUnitIntent::Assert: { + action = TelemetryTraceLogger::TestAction; TestSettingsResult settingsResult = unitProcessor.TestSettings(); - + if (settingsResult.TestResult() == ConfigurationTestResult::Positive) { - return true; + result = true; } else if (settingsResult.TestResult() == ConfigurationTestResult::Negative) { - unitInfo.ResultInformation->ResultCode(WINGET_CONFIG_ERROR_ASSERTION_FAILED); - return false; + unitInfo.ResultInformation->Initialize(WINGET_CONFIG_ERROR_ASSERTION_FAILED, ConfigurationUnitResultSource::Precondition); } else if (settingsResult.TestResult() == ConfigurationTestResult::Failed) { unitInfo.ResultInformation->Initialize(settingsResult.ResultInformation()); - return false; } else { - unitInfo.ResultInformation->ResultCode(E_UNEXPECTED); - return false; + unitInfo.ResultInformation->Initialize(E_UNEXPECTED, ConfigurationUnitResultSource::Internal); } } + break; + case ConfigurationUnitIntent::Inform: { // Force the processor to retrieve the settings + action = TelemetryTraceLogger::GetAction; GetSettingsResult settingsResult = unitProcessor.GetSettings(); if (SUCCEEDED(settingsResult.ResultInformation().ResultCode())) { - return true; + result = true; } else { unitInfo.ResultInformation->Initialize(settingsResult.ResultInformation()); - return false; } } + break; + case ConfigurationUnitIntent::Apply: { + action = TelemetryTraceLogger::TestAction; TestSettingsResult testSettingsResult = unitProcessor.TestSettings(); if (testSettingsResult.TestResult() == ConfigurationTestResult::Positive) { unitInfo.Result->PreviouslyInDesiredState(true); - return true; + result = true; } else if (testSettingsResult.TestResult() == ConfigurationTestResult::Negative) { + action = TelemetryTraceLogger::ApplyAction; ApplySettingsResult applySettingsResult = unitProcessor.ApplySettings(); if (SUCCEEDED(applySettingsResult.ResultInformation().ResultCode())) { unitInfo.Result->RebootRequired(applySettingsResult.RebootRequired()); - return true; + result = true; } else { unitInfo.ResultInformation->Initialize(applySettingsResult.ResultInformation()); - return false; } } else if (testSettingsResult.TestResult() == ConfigurationTestResult::Failed) { unitInfo.ResultInformation->Initialize(testSettingsResult.ResultInformation()); - return false; } else { - unitInfo.ResultInformation->ResultCode(E_UNEXPECTED); - return false; + unitInfo.ResultInformation->Initialize(E_UNEXPECTED, ConfigurationUnitResultSource::Internal); } } + break; + default: - unitInfo.ResultInformation->ResultCode(E_UNEXPECTED); - return false; + unitInfo.ResultInformation->Initialize(E_UNEXPECTED, ConfigurationUnitResultSource::Internal); + break; } } catch (...) { ExtractUnitResultInformation(std::current_exception(), unitInfo.ResultInformation); - return false; } + + m_telemetry.LogConfigUnitRunIfAppropriate(m_configurationSet.InstanceIdentifier(), unitInfo.Unit, ConfigurationUnitIntent::Apply, action, *unitInfo.ResultInformation); + return result; } void ConfigurationSetApplyProcessor::SendProgress(ConfigurationSetState state) diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetApplyProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationSetApplyProcessor.h @@ -6,6 +6,7 @@ #include "ApplyConfigurationSetResult.h" #include "ApplyConfigurationUnitResult.h" #include "ConfigurationUnitResultInformation.h" +#include "Telemetry/Telemetry.h" #include <map> #include <string> @@ -23,7 +24,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation using result_type = decltype(make_self<wil::details::module_count_wrapper<implementation::ApplyConfigurationSetResult>>()); - ConfigurationSetApplyProcessor(const ConfigurationSet& configurationSet, IConfigurationSetProcessor&& setProcessor, result_type result, const std::function<void(ConfigurationSetChangeData)>& progress); + ConfigurationSetApplyProcessor(const ConfigurationSet& configurationSet, const TelemetryTraceLogger& telemetry, IConfigurationSetProcessor&& setProcessor, result_type result, const std::function<void(ConfigurationSetChangeData)>& progress); // Processes the apply for the configuration set. void Process(); @@ -90,7 +91,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation void SendProgress(ConfigurationSetState state); void SendProgress(ConfigurationUnitState state, const UnitInfo& unitInfo); + ConfigurationSet m_configurationSet; IConfigurationSetProcessor m_setProcessor; + const TelemetryTraceLogger& m_telemetry; result_type m_result; std::function<void(ConfigurationSetChangeData)> m_progress; std::vector<UnitInfo> m_unitInfo; diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnit.h b/src/Microsoft.Management.Configuration/ConfigurationUnit.h @@ -10,6 +10,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation { struct ConfigurationUnit : ConfigurationUnitT<ConfigurationUnit> { + using ConfigurationUnitResultInformation = Configuration::ConfigurationUnitResultInformation; + ConfigurationUnit(); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnitResultInformation.cpp b/src/Microsoft.Management.Configuration/ConfigurationUnitResultInformation.cpp @@ -3,28 +3,58 @@ #include "pch.h" #include "ConfigurationUnitResultInformation.h" #include "ConfigurationUnitResultInformation.g.cpp" +#include "AppInstallerErrors.h" namespace winrt::Microsoft::Management::Configuration::implementation { + namespace + { + ConfigurationUnitResultSource FromHRESULT(hresult resultCode) + { + switch (resultCode) + { + case WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED: + case WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY: + case WINGET_CONFIG_ERROR_UNIT_MULTIPLE_MATCHES: + case WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE: + return ConfigurationUnitResultSource::ConfigurationSet; + case WINGET_CONFIG_ERROR_UNIT_MODULE_CONFLICT: + return ConfigurationUnitResultSource::SystemState; + } + + return ConfigurationUnitResultSource::Internal; + } + } + void ConfigurationUnitResultInformation::Initialize(const Configuration::ConfigurationUnitResultInformation& other) { m_resultCode = other.ResultCode(); m_description = other.Description(); + m_details = other.Details(); + m_resultSource = other.ResultSource(); } void ConfigurationUnitResultInformation::Initialize(hresult resultCode, std::wstring_view description) { m_resultCode = resultCode; m_description = description; + m_resultSource = FromHRESULT(resultCode); } void ConfigurationUnitResultInformation::Initialize(hresult resultCode, hstring description) { m_resultCode = resultCode; m_description = description; + m_resultSource = FromHRESULT(resultCode); + } + + void ConfigurationUnitResultInformation::Initialize(hresult resultCode, ConfigurationUnitResultSource resultSource) + { + m_resultCode = resultCode; + m_resultSource = resultSource; } - hresult ConfigurationUnitResultInformation::ResultCode() + hresult ConfigurationUnitResultInformation::ResultCode() const { return m_resultCode; } @@ -43,4 +73,24 @@ namespace winrt::Microsoft::Management::Configuration::implementation { m_description = value; } + + hstring ConfigurationUnitResultInformation::Details() + { + return m_details; + } + + void ConfigurationUnitResultInformation::Details(hstring value) + { + m_details = value; + } + + ConfigurationUnitResultSource ConfigurationUnitResultInformation::ResultSource() const + { + return m_resultSource; + } + + void ConfigurationUnitResultInformation::ResultSource(ConfigurationUnitResultSource value) + { + m_resultSource = value; + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnitResultInformation.h b/src/Microsoft.Management.Configuration/ConfigurationUnitResultInformation.h @@ -13,18 +13,27 @@ namespace winrt::Microsoft::Management::Configuration::implementation void Initialize(const Configuration::ConfigurationUnitResultInformation& other); void Initialize(hresult resultCode, std::wstring_view description); void Initialize(hresult resultCode, hstring description); + void Initialize(hresult resultCode, ConfigurationUnitResultSource resultSource); #endif - hresult ResultCode(); + hresult ResultCode() const; void ResultCode(hresult resultCode); hstring Description(); void Description(hstring value); + hstring Details(); + void Details(hstring value); + + ConfigurationUnitResultSource ResultSource() const; + void ResultSource(ConfigurationUnitResultSource value); + #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: hresult m_resultCode; hstring m_description; + hstring m_details; + ConfigurationUnitResultSource m_resultSource = ConfigurationUnitResultSource::None; #endif }; } diff --git a/src/Microsoft.Management.Configuration/ExceptionResultHelpers.h b/src/Microsoft.Management.Configuration/ExceptionResultHelpers.h @@ -17,6 +17,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation { unitResult->Initialize(hre.code(), hre.message()); } + catch (const std::exception& ex) + { + unitResult->Initialize(E_FAIL, AppInstaller::Utility::ConvertToUTF16(ex.what())); + } catch (...) { unitResult->Initialize(E_FAIL, hstring{}); diff --git a/src/Microsoft.Management.Configuration/GetConfigurationUnitSettingsResult.cpp b/src/Microsoft.Management.Configuration/GetConfigurationUnitSettingsResult.cpp @@ -17,7 +17,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_resultInformation = resultInformation; } - Configuration::ConfigurationUnitResultInformation GetConfigurationUnitSettingsResult::ResultInformation() + Configuration::ConfigurationUnitResultInformation GetConfigurationUnitSettingsResult::ResultInformation() const { return m_resultInformation; } diff --git a/src/Microsoft.Management.Configuration/GetConfigurationUnitSettingsResult.h b/src/Microsoft.Management.Configuration/GetConfigurationUnitSettingsResult.h @@ -17,7 +17,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation void Settings(Windows::Foundation::Collections::ValueSet&& value); #endif - ConfigurationUnitResultInformation ResultInformation(); + ConfigurationUnitResultInformation ResultInformation() const; Windows::Foundation::Collections::ValueSet Settings(); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl @@ -49,6 +49,27 @@ namespace Microsoft.Management.Configuration Load, }; + // The source of a result; for instance, the part of the system that generated a failure. + [contract(Microsoft.Management.Configuration.Contract, 1)] + enum ConfigurationUnitResultSource + { + // The source is not known, or more likely, there was no failure. + None, + // The result came from inside the configuration system; this is likely a bug. + Internal, + // The configuration set was ill formed. For instance, referencing a configuration unit + // that does not exist or a dependency that is not present. + ConfigurationSet, + // The external module that processes the configuration unit generated the result. + UnitProcessing, + // The system state is causing the error. + SystemState, + // The configuration unit was not run due to a precondition not being met. + // For example, when an assert in the configuration set is not in the desired state, + // all of the units with Apply intent will have this set. + Precondition, + }; + // Information on a result for a single unit of configuration. [contract(Microsoft.Management.Configuration.Contract, 1)] runtimeclass ConfigurationUnitResultInformation @@ -56,10 +77,16 @@ namespace Microsoft.Management.Configuration // The error code of the failure. HRESULT ResultCode; - // The description of the failure. + // The short description of the failure. String Description; + + // A more detailed error message appropriate for diagnosing the root cause of an error. + String Details; + + // The source of the result. + ConfigurationUnitResultSource ResultSource; } - + // Provides information for a specific configuration unit setting. [contract(Microsoft.Management.Configuration.Contract, 1)] interface IConfigurationUnitSettingDetails @@ -597,6 +624,15 @@ namespace Microsoft.Management.Configuration // Indicates the minimum importance desired for diagnostics. DiagnosticLevel MinimumLevel; + // Set the caller to used to identify the usage in telemetry events. + String Caller; + + // The identifier for the current activity, enabling multiple calls into the processor to be correlated. + Guid ActivityIdentifier; + + // If true, ETW events will be generated. Some of those events may be sent to Microsoft depending on the system settings. + Boolean GenerateTelemetryEvents; + // Only top level configuration changes are sent to this event. // This includes things like: creation of a new set for intent to run, start/stop of a set for application or test, deletion of a not started set. event Windows.Foundation.TypedEventHandler<ConfigurationSet, ConfigurationChangeData> ConfigurationChange; diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -135,6 +135,11 @@ <OptimizeReferences>true</OptimizeReferences> </Link> </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(WingetDisableTestHooks)'=='true'"> + <ClCompile> + <PreprocessorDefinitions>AICLI_DISABLE_TEST_HOOKS;%(PreprocessorDefinitions)</PreprocessorDefinitions> + </ClCompile> + </ItemDefinitionGroup> <ItemGroup> <ClInclude Include="ApplyConfigurationSetResult.h" /> <ClInclude Include="ApplyConfigurationUnitResult.h" /> @@ -161,6 +166,8 @@ <ClInclude Include="MutableFlag.h" /> <ClInclude Include="OpenConfigurationSetResult.h" /> <ClInclude Include="pch.h" /> + <ClInclude Include="Telemetry\Telemetry.h" /> + <ClInclude Include="Telemetry\TraceLogging.h" /> <ClInclude Include="TestConfigurationSetResult.h" /> <ClInclude Include="TestConfigurationUnitResult.h" /> <ClInclude Include="TestSettingsResult.h" /> @@ -192,6 +199,8 @@ <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="$(GeneratedFilesDir)module.g.cpp" /> + <ClCompile Include="Telemetry\Telemetry.cpp" /> + <ClCompile Include="Telemetry\TraceLogging.cpp" /> <ClCompile Include="TestConfigurationSetResult.cpp" /> <ClCompile Include="TestConfigurationUnitResult.cpp" /> <ClCompile Include="TestSettingsResult.cpp" /> diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -78,6 +78,12 @@ <ClCompile Include="GetConfigurationSetDetailsResult.cpp"> <Filter>API Source</Filter> </ClCompile> + <ClCompile Include="Telemetry\TraceLogging.cpp"> + <Filter>Telemetry</Filter> + </ClCompile> + <ClCompile Include="Telemetry\Telemetry.cpp"> + <Filter>Telemetry</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h" /> @@ -162,6 +168,12 @@ <ClInclude Include="GetConfigurationSetDetailsResult.h"> <Filter>API Headers</Filter> </ClInclude> + <ClInclude Include="Telemetry\TraceLogging.h"> + <Filter>Telemetry</Filter> + </ClInclude> + <ClInclude Include="Telemetry\Telemetry.h"> + <Filter>Telemetry</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" /> @@ -184,5 +196,8 @@ <Filter Include="Parser"> <UniqueIdentifier>{b31f8336-b4d8-4c05-b08d-6b82c550a30b}</UniqueIdentifier> </Filter> + <Filter Include="Telemetry"> + <UniqueIdentifier>{5a02f1a5-14f3-4a28-8bed-212f3e6b1a00}</UniqueIdentifier> + </Filter> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Microsoft.Management.Configuration/Telemetry/Telemetry.cpp b/src/Microsoft.Management.Configuration/Telemetry/Telemetry.cpp @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Telemetry.h" +#include "TraceLogging.h" +#include <AppInstallerErrors.h> +#include <AppInstallerLogging.h> +#include <AppInstallerStrings.h> +#include <winget/Runtime.h> + +#define AICLI_TraceLoggingStringView(_sv_,_name_) TraceLoggingCountedUtf8String(_sv_.data(), static_cast<ULONG>(_sv_.size()), _name_) +#define AICLI_TraceLoggingWStringView(_sv_,_name_) TraceLoggingCountedWideString(_sv_.data(), static_cast<ULONG>(_sv_.size()), _name_) + +#define AICLI_TraceLoggingProcessingSummaryForIntent(_forIntent_,_name_,_pluralName_) \ + TraceLoggingUInt32(_forIntent_.Count, _name_ ## "Count"), \ + TraceLoggingUInt32(_forIntent_.Run, _pluralName_ ## "Run"), \ + TraceLoggingUInt32(_forIntent_.Failed, _pluralName_ ## "Failed") + +#define AICLI_TraceLoggingWriteActivity(_eventName_,...) TraceLoggingWriteActivity(\ +g_hTraceProvider,\ +_eventName_,\ +GetActivityId(),\ +nullptr,\ +TraceLoggingCountedUtf8String(m_version.c_str(), static_cast<ULONG>(m_version.size()), "CodeVersion"),\ +TraceLoggingCountedUtf8String(m_caller.c_str(), static_cast<ULONG>(m_caller.size()), "Caller"),\ +__VA_ARGS__) + +#ifdef AICLI_DISABLE_TEST_HOOKS + +#define WinGet_EventItem(_value_,_name_) +#define WinGet_SummaryForIntentItem(_forIntent_,_name_,_pluralName_) +#define WinGet_WriteEventToDiagnostics(_eventName_,...) + +#else + +struct WinGetAbsorbVA_ARGSCommas +{ + WinGetAbsorbVA_ARGSCommas(int, int) {} +}; + +inline std::ostream& operator<<(std::ostream& out, const WinGetAbsorbVA_ARGSCommas&) { return out; } +inline std::ostream& operator<<(std::ostream& out, std::wstring_view value) { out << AppInstaller::Utility::ConvertToUTF8(value); return out; } + +#define WinGet_EventItem(_value_,_name_) \ + 0) << (_name_) << ": " << (_value_) << '\n' << WinGetAbsorbVA_ARGSCommas(0 + +#define WinGet_SummaryForIntentItem(_forIntent_,_name_,_pluralName_) \ + WinGet_EventItem(_forIntent_.Count, _name_ ## "Count"), \ + WinGet_EventItem(_forIntent_.Run, _pluralName_ ## "Run"), \ + WinGet_EventItem(_forIntent_.Failed, _pluralName_ ## "Failed") + +#define WinGet_WriteEventToDiagnostics(_eventName_,...) \ +{ \ + std::ostringstream _debugEventStream; \ + _debugEventStream << \ + "#DebugEventStream\n" << \ + "Event: " << (_eventName_) << '\n' << \ + "ActivityID: " << *GetActivityId() << '\n' << \ + "CodeVersion: " << m_version << '\n' << \ + "Caller: " << m_caller << '\n' \ + << WinGetAbsorbVA_ARGSCommas(0, __VA_ARGS__ ,0) \ + ; \ + AICLI_LOG_LARGE_STRING(Config, Verbose, , _debugEventStream.str()); \ +} + +#endif + +using namespace std::string_view_literals; + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + namespace + { + // The data collected from running through a set of results. + struct ConfigRunSummaryData + { + hresult Result = S_OK; + ConfigurationUnitResultSource FailurePoint = ConfigurationUnitResultSource::None; + TelemetryTraceLogger::ProcessingSummaryForIntent AssertSummary{ ConfigurationUnitIntent::Assert }; + TelemetryTraceLogger::ProcessingSummaryForIntent InformSummary{ ConfigurationUnitIntent::Inform }; + TelemetryTraceLogger::ProcessingSummaryForIntent ApplySummary{ ConfigurationUnitIntent::Apply }; + }; + + size_t GetPriority(ConfigurationUnitResultSource source) + { + switch (source) + { + case ConfigurationUnitResultSource::Internal: return 0; + case ConfigurationUnitResultSource::UnitProcessing: return 100; + case ConfigurationUnitResultSource::SystemState: return 200; + case ConfigurationUnitResultSource::ConfigurationSet: return 300; + case ConfigurationUnitResultSource::Precondition: return 400; + default: return 500; + case ConfigurationUnitResultSource::None: return 600; + } + } + + bool FirstHasPriority(ConfigurationUnitResultSource first, ConfigurationUnitResultSource second) + { + return GetPriority(first) < GetPriority(second); + } + + void ProcessUnitResult(const Configuration::ConfigurationUnit unit, Configuration::ConfigurationUnitResultInformation resultInformation, ConfigRunSummaryData& result) + { + hresult resultCode = resultInformation.ResultCode(); + if (FAILED(resultCode)) + { + if (result.Result == S_OK || result.Result == resultCode) + { + result.Result = resultCode; + } + else + { + result.Result = WINGET_CONFIG_ERROR_SET_APPLY_FAILED; + } + } + + ConfigurationUnitResultSource unitFailurePoint = resultInformation.ResultSource(); + if (FirstHasPriority(unitFailurePoint, result.FailurePoint)) + { + result.FailurePoint = unitFailurePoint; + } + + TelemetryTraceLogger::ProcessingSummaryForIntent* summaryItem = nullptr; + switch (unit.Intent()) + { + case ConfigurationUnitIntent::Assert: + summaryItem = &result.AssertSummary; + break; + case ConfigurationUnitIntent::Inform: + summaryItem = &result.InformSummary; + break; + case ConfigurationUnitIntent::Apply: + summaryItem = &result.ApplySummary; + break; + default: + return; + } + + summaryItem->Count++; + + ConfigurationUnitResultSource resultSource = resultInformation.ResultSource(); + if (resultSource != ConfigurationUnitResultSource::Precondition && + resultSource != ConfigurationUnitResultSource::ConfigurationSet) + { + summaryItem->Run++; + } + + if (FAILED(resultCode)) + { + summaryItem->Failed++; + } + } + + // Runs through a set of results, summarizing them. + template <typename Enumerable> + ConfigRunSummaryData ProcessRunResult(const Enumerable& results) + { + ConfigRunSummaryData result; + + for (const auto& item : results) + { + ProcessUnitResult(item.Unit(), item.ResultInformation(), result); + } + + return result; + } + } + + TelemetryTraceLogger::TelemetryTraceLogger() + { + std::ignore = CoCreateGuid(&m_activityId); + m_version = AppInstaller::Runtime::GetClientVersion(); + } + + void TelemetryTraceLogger::SetActivityId(const guid& value) + { + m_activityId = value; + } + + const GUID* TelemetryTraceLogger::GetActivityId() const + { + return &m_activityId; + } + + bool TelemetryTraceLogger::EnableRuntime(bool value) + { + return m_isRuntimeEnabled.exchange(value); + } + + bool TelemetryTraceLogger::IsEnabled() const + { + return m_isRuntimeEnabled; + } + + void TelemetryTraceLogger::SetCaller(std::string_view caller) + { + m_caller = caller; + } + + std::string_view TelemetryTraceLogger::GetCaller() const + { + return m_caller; + } + + void TelemetryTraceLogger::LogConfigUnitRun( + const guid& setIdentifier, + const guid& unitIdentifier, + hstring unitName, + hstring moduleName, + ConfigurationUnitIntent unitIntent, + ConfigurationUnitIntent runIntent, + std::string_view action, + hresult result, + ConfigurationUnitResultSource failurePoint, + std::wstring_view settingNames) const noexcept try + { + if (IsTelemetryEnabled()) + { + AICLI_TraceLoggingWriteActivity( + "ConfigUnitRun", + TraceLoggingGuid(setIdentifier, "SetID"), + TraceLoggingGuid(unitIdentifier, "UnitID"), + AICLI_TraceLoggingWStringView(unitName, "UnitName"), + AICLI_TraceLoggingWStringView(moduleName, "ModuleName"), + TraceLoggingInt32(static_cast<int32_t>(unitIntent), "UnitIntent"), + TraceLoggingInt32(static_cast<int32_t>(runIntent), "RunIntent"), + AICLI_TraceLoggingStringView(action, "Action"), + TraceLoggingHResult(result, "Result"), + TraceLoggingInt32(static_cast<int32_t>(failurePoint), "FailurePoint"), + AICLI_TraceLoggingWStringView(settingNames, "SettingsProvided"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES)); + + // Keep in sync with above event! + WinGet_WriteEventToDiagnostics( + "ConfigUnitRun", + WinGet_EventItem(setIdentifier, "SetID"), + WinGet_EventItem(unitIdentifier, "UnitID"), + WinGet_EventItem(unitName, "UnitName"), + WinGet_EventItem(moduleName, "ModuleName"), + WinGet_EventItem(static_cast<int32_t>(unitIntent), "UnitIntent"), + WinGet_EventItem(static_cast<int32_t>(runIntent), "RunIntent"), + WinGet_EventItem(action, "Action"), + WinGet_EventItem(result, "Result"), + WinGet_EventItem(static_cast<int32_t>(failurePoint), "FailurePoint"), + WinGet_EventItem(settingNames, "SettingsProvided")); + } + } + CATCH_LOG(); + + void TelemetryTraceLogger::LogConfigUnitRunIfAppropriate( + const guid& setIdentifier, + const Configuration::ConfigurationUnit& unit, + ConfigurationUnitIntent runIntent, + std::string_view action, + const Configuration::ConfigurationUnitResultInformation& resultInformation) const noexcept try + { + // We only want to send telemetry for failures of publicly available units. + if (!IsTelemetryEnabled() || SUCCEEDED(static_cast<int32_t>(resultInformation.ResultCode()))) + { + return; + } + + IConfigurationUnitProcessorDetails details = unit.Details(); + if (!details || !details.IsPublic()) + { + return; + } + + // Create a single string from the set of top level setting names, ex. "a|b|c". + const winrt::Windows::Foundation::Collections::ValueSet& settings = unit.Settings(); + std::wostringstream strstr; + + for (const auto& setting : settings) + { + strstr << static_cast<std::wstring_view>(setting.Key()) << L'|'; + } + std::wstring allSettingsNames = strstr.str(); + if (!allSettingsNames.empty()) + { + allSettingsNames.pop_back(); + } + + LogConfigUnitRun(setIdentifier, unit.InstanceIdentifier(), unit.UnitName(), details.ModuleName(), unit.Intent(), runIntent, action, resultInformation.ResultCode(), resultInformation.ResultSource(), allSettingsNames); + } + CATCH_LOG(); + + void TelemetryTraceLogger::LogConfigProcessingSummary( + const guid& setIdentifier, + bool fromHistory, + ConfigurationUnitIntent runIntent, + hresult result, + ConfigurationUnitResultSource failurePoint, + const ProcessingSummaryForIntent& assertSummary, + const ProcessingSummaryForIntent& informSummary, + const ProcessingSummaryForIntent& applySummary) const noexcept try + { + if (IsTelemetryEnabled()) + { + AICLI_TraceLoggingWriteActivity( + "ConfigProcessingSummary", + TraceLoggingGuid(setIdentifier, "SetID"), + TraceLoggingBool(fromHistory, "FromHistory"), + TraceLoggingInt32(static_cast<int32_t>(runIntent), "RunIntent"), + TraceLoggingHResult(result, "Result"), + TraceLoggingInt32(static_cast<int32_t>(failurePoint), "FailurePoint"), + AICLI_TraceLoggingProcessingSummaryForIntent(assertSummary, "Assert", "Asserts"), + AICLI_TraceLoggingProcessingSummaryForIntent(informSummary, "Inform", "Informs"), + AICLI_TraceLoggingProcessingSummaryForIntent(applySummary, "Apply", "Applies"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES)); + + // Keep in sync with above event! + WinGet_WriteEventToDiagnostics( + "ConfigProcessingSummary", + WinGet_EventItem(setIdentifier, "SetID"), + WinGet_EventItem(fromHistory, "FromHistory"), + WinGet_EventItem(static_cast<int32_t>(runIntent), "RunIntent"), + WinGet_EventItem(result, "Result"), + WinGet_EventItem(static_cast<int32_t>(failurePoint), "FailurePoint"), + WinGet_SummaryForIntentItem(assertSummary, "Assert", "Asserts"), + WinGet_SummaryForIntentItem(informSummary, "Inform", "Informs"), + WinGet_SummaryForIntentItem(applySummary, "Apply", "Applies")); + } + } + CATCH_LOG(); + + void TelemetryTraceLogger::LogConfigProcessingSummaryForTest( + const ConfigurationSet& configurationSet, + const TestConfigurationSetResult& result) const noexcept try + { + if (!IsTelemetryEnabled()) + { + return; + } + + ConfigRunSummaryData summaryData = ProcessRunResult(result.UnitResults()); + + LogConfigProcessingSummary(configurationSet.InstanceIdentifier(), configurationSet.IsFromHistory(), ConfigurationUnitIntent::Assert, + summaryData.Result, summaryData.FailurePoint, summaryData.AssertSummary, summaryData.InformSummary, summaryData.ApplySummary); + } + CATCH_LOG(); + + void TelemetryTraceLogger::LogConfigProcessingSummaryForApply( + const ConfigurationSet& configurationSet, + const ApplyConfigurationSetResult& result) const noexcept try + { + if (!IsTelemetryEnabled()) + { + return; + } + + ConfigRunSummaryData summaryData = ProcessRunResult(result.UnitResults()); + + LogConfigProcessingSummary(configurationSet.InstanceIdentifier(), configurationSet.IsFromHistory(), ConfigurationUnitIntent::Apply, + result.ResultCode(), summaryData.FailurePoint, summaryData.AssertSummary, summaryData.InformSummary, summaryData.ApplySummary); + } + CATCH_LOG(); + + bool TelemetryTraceLogger::IsTelemetryEnabled() const noexcept + { +#ifdef AICLI_DISABLE_TEST_HOOKS + return g_IsTelemetryProviderEnabled && m_isRuntimeEnabled; +#else + // For testing, only use the local enable state. + return m_isRuntimeEnabled; +#endif + } +} diff --git a/src/Microsoft.Management.Configuration/Telemetry/Telemetry.h b/src/Microsoft.Management.Configuration/Telemetry/Telemetry.h @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerLanguageUtilities.h> +#include <winrt/Microsoft.Management.Configuration.h> +#include <winrt/Windows.Foundation.Collections.h> +#include "ConfigurationUnitResultInformation.h" +#include "ConfigurationSet.h" +#include "TestConfigurationSetResult.h" +#include "ApplyConfigurationSetResult.h" + +#include <cguid.h> +#include <string> +#include <string_view> +#include <vector> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Provides the ability to write telemetry events. + struct TelemetryTraceLogger + { + TelemetryTraceLogger(); + + TelemetryTraceLogger(const TelemetryTraceLogger&) = default; + TelemetryTraceLogger& operator=(const TelemetryTraceLogger&) = default; + + TelemetryTraceLogger(TelemetryTraceLogger&&) = default; + TelemetryTraceLogger& operator=(TelemetryTraceLogger&&) = default; + + // Control whether this trace logger is enabled at runtime. + // Returns the previous value. + bool EnableRuntime(bool value); + + // Returns a value indicating whether the logger is enabled. + bool IsEnabled() const; + + // Sets the current activity identifier. + void SetActivityId(const guid& value); + + // Return address of m_activityId + const GUID* GetActivityId() const; + + // Store the passed in name of the caller + void SetCaller(std::string_view caller); + + // Get the current caller value + std::string_view GetCaller() const; + + static constexpr std::string_view GetAction = "get"; + static constexpr std::string_view ApplyAction = "apply"; + static constexpr std::string_view TestAction = "test"; + + // Logs information about running a configuration unit. + // The caller is expected to only call this for failures from publicly available units. + void LogConfigUnitRun( + const guid& setIdentifier, + const guid& unitIdentifier, + hstring unitName, + hstring moduleName, + ConfigurationUnitIntent unitIntent, + ConfigurationUnitIntent runIntent, + std::string_view action, + hresult result, + ConfigurationUnitResultSource failurePoint, + std::wstring_view settingNames) const noexcept; + + // Logs information about running a configuration unit in the appropriate conditions. + void LogConfigUnitRunIfAppropriate( + const guid& setIdentifier, + const Configuration::ConfigurationUnit& unit, + ConfigurationUnitIntent runIntent, + std::string_view action, + const Configuration::ConfigurationUnitResultInformation& resultInformation) const noexcept; + + // The summary information for a specific unit intent. + struct ProcessingSummaryForIntent + { + ConfigurationUnitIntent Intent; + uint32_t Count; + uint32_t Run; + uint32_t Failed; + }; + + // Logs a processing summary event for a configuration set. + void LogConfigProcessingSummary( + const guid& setIdentifier, + bool fromHistory, + ConfigurationUnitIntent runIntent, + hresult result, + ConfigurationUnitResultSource failurePoint, + const ProcessingSummaryForIntent& assertSummary, + const ProcessingSummaryForIntent& informSummary, + const ProcessingSummaryForIntent& applySummary) const noexcept; + + // Logs a processing summary event for a configuration set test run. + void LogConfigProcessingSummaryForTest( + const ConfigurationSet& configurationSet, + const TestConfigurationSetResult& result) const noexcept; + + // Logs a processing summary event for a configuration set apply run. + void LogConfigProcessingSummaryForApply( + const ConfigurationSet& configurationSet, + const ApplyConfigurationSetResult& result) const noexcept; + + protected: + bool IsTelemetryEnabled() const noexcept; + + CopyConstructibleAtomic<bool> m_isRuntimeEnabled{ true }; + + GUID m_activityId = GUID_NULL; + std::string m_version; + std::string m_caller; + }; +} diff --git a/src/Microsoft.Management.Configuration/Telemetry/TraceLogging.cpp b/src/Microsoft.Management.Configuration/Telemetry/TraceLogging.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" +#include "TraceLogging.h" + +// GUID for Microsoft.Management.Configuration : {9be929c4-3582-4629-aaa2-f427a5032b33} +TRACELOGGING_DEFINE_PROVIDER( + g_hTraceProvider, + "Microsoft.Management.Configuration", + (0x9be929c4, 0x3582, 0x4629, 0xaa, 0xa2, 0xf4, 0x27, 0xa5, 0x03, 0x2b, 0x33), + TraceLoggingOptionMicrosoftTelemetry()); + +bool g_IsTelemetryProviderEnabled{}; +UCHAR g_TelemetryProviderLevel{}; +ULONGLONG g_TelemetryProviderMatchAnyKeyword{}; + +struct TraceProvider +{ + TraceProvider(); + + ~TraceProvider(); +}; + +TraceProvider g_TraceProvider{}; + +void WINAPI TelemetryProviderEnabledCallback( + _In_ LPCGUID /*sourceId*/, + _In_ ULONG isEnabled, + _In_ UCHAR level, + _In_ ULONGLONG matchAnyKeyword, + _In_ ULONGLONG /*matchAllKeywords*/, + _In_opt_ PEVENT_FILTER_DESCRIPTOR /*filterData*/, + _In_opt_ PVOID /*callbackContext*/) +{ + g_IsTelemetryProviderEnabled = !!isEnabled; + g_TelemetryProviderLevel = level; + g_TelemetryProviderMatchAnyKeyword = matchAnyKeyword; +} + +TraceProvider::TraceProvider() +{ + TraceLoggingRegisterEx(g_hTraceProvider, TelemetryProviderEnabledCallback, nullptr); +} + +TraceProvider::~TraceProvider() +{ + TraceLoggingUnregister(g_hTraceProvider); +} diff --git a/src/Microsoft.Management.Configuration/Telemetry/TraceLogging.h b/src/Microsoft.Management.Configuration/Telemetry/TraceLogging.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include <Telemetry/WinEventLogLevels.h> +#include <TraceLoggingProvider.h> + +#include <Telemetry/MicrosoftTelemetry.h> + +// Keywords +#define KEYWORD_REPEATER 0x0000000000000001 +#define KEYWORD_SCROLLER 0x0000000000000002 +#define KEYWORD_PTR 0x0000000000000004 +#define KEYWORD_SCROLLVIEWER 0x0000000000000008 +#define KEYWORD_SWIPECONTROL 0x0000000000000010 +#define KEYWORD_COMMANDBARFLYOUT 0x0000000000000020 + +// Common output formats +#define TRACE_MSG_METH L"%s[0x%p]()\n" +#define TRACE_MSG_METH_DBL L"%s[0x%p](%lf)\n" +#define TRACE_MSG_METH_DBL_DBL L"%s[0x%p](%lf, %lf)\n" +#define TRACE_MSG_METH_DBL_INT L"%s[0x%p](%lf, %d)\n" +#define TRACE_MSG_METH_DBL_DBL_INT L"%s[0x%p](%lf, %lf, %d)\n" +#define TRACE_MSG_METH_DBL_DBL_FLT L"%s[0x%p](%lf, %lf, %f)\n" +#define TRACE_MSG_METH_DBL_DBL_STR L"%s[0x%p](%lf, %lf, %s)\n" +#define TRACE_MSG_METH_FLT L"%s[0x%p](%f)\n" +#define TRACE_MSG_METH_FLT_FLT L"%s[0x%p](%f, %f)\n" +#define TRACE_MSG_METH_FLT_FLT_FLT L"%s[0x%p](%f, %f, %f)\n" +#define TRACE_MSG_METH_FLT_FLT_FLT_FLT L"%s[0x%p](%f, %f, %f, %f)\n" +#define TRACE_MSG_METH_FLT_FLT_STR_INT L"%s[0x%p](%f, %f, %s, %d)\n" +#define TRACE_MSG_METH_INT L"%s[0x%p](%d)\n" +#define TRACE_MSG_METH_INT_INT L"%s[0x%p](%d, %d)\n" +#define TRACE_MSG_METH_PTR L"%s[0x%p](0x%p)\n" +#define TRACE_MSG_METH_PTR_PTR L"%s[0x%p](0x%p, 0x%p)\n" +#define TRACE_MSG_METH_PTR_DBL L"%s[0x%p](0x%p, %lf)\n" +#define TRACE_MSG_METH_PTR_INT L"%s[0x%p](0x%p, %d)\n" +#define TRACE_MSG_METH_PTR_STR L"%s[0x%p](0x%p, %s)\n" +#define TRACE_MSG_METH_STR L"%s[0x%p](%s)\n" +#define TRACE_MSG_METH_STR_STR L"%s[0x%p](%s, %s)\n" +#define TRACE_MSG_METH_STR_DBL L"%s[0x%p](%s, %lf)\n" +#define TRACE_MSG_METH_STR_FLT L"%s[0x%p](%s, %f)\n" +#define TRACE_MSG_METH_STR_INT L"%s[0x%p](%s, %d)\n" +#define TRACE_MSG_METH_STR_STR_STR L"%s[0x%p](%s, %s, %s)\n" +#define TRACE_MSG_METH_STR_INT_INT L"%s[0x%p](%s, %d, %d)\n" +#define TRACE_MSG_METH_STR_FLT_FLT L"%s[0x%p](%s, %f, %f)\n" +#define TRACE_MSG_METH_STR_STR_FLT L"%s[0x%p](%s, %s, %f)\n" +#define TRACE_MSG_METH_STR_STR_INT_INT L"%s[0x%p](%s, %s, %d, %d)\n" + +#define TRACE_MSG_METH_METH L"%s[0x%p] - calls %s()\n" +#define TRACE_MSG_METH_METH_INT L"%s[0x%p] - calls %s(%d)\n" +#define TRACE_MSG_METH_METH_STR L"%s[0x%p] - calls %s(%s)\n" +#define TRACE_MSG_METH_METH_STR_STR L"%s[0x%p] - calls %s(%s, %s)\n" +#define TRACE_MSG_METH_METH_FLT_STR L"%s[0x%p] - calls %s(%f, %s)\n" +#define TRACE_MSG_METH_METH_FLT_FLT_FLT L"%s[0x%p] - calls %s(%f, %f, %f)\n" + +// Current method name +#define METH_NAME StringUtil::Utf8ToUtf16(__FUNCTION__).c_str() + +// TraceLogging provider name for telemetry. +#define TELEMETRY_PROVIDER_NAME "Microsoft.Management.Configuration" + +TRACELOGGING_DECLARE_PROVIDER(g_hTraceProvider); +extern bool g_IsTelemetryProviderEnabled; +extern UCHAR g_TelemetryProviderLevel; +extern ULONGLONG g_TelemetryProviderMatchAnyKeyword; diff --git a/src/Microsoft.Management.Configuration/TestConfigurationSetResult.cpp b/src/Microsoft.Management.Configuration/TestConfigurationSetResult.cpp @@ -44,12 +44,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } - Windows::Foundation::Collections::IVectorView<TestConfigurationUnitResult> TestConfigurationSetResult::UnitResults() + Windows::Foundation::Collections::IVectorView<TestConfigurationUnitResult> TestConfigurationSetResult::UnitResults() const { return m_unitResults.GetView(); } - ConfigurationTestResult TestConfigurationSetResult::TestResult() + ConfigurationTestResult TestConfigurationSetResult::TestResult() const { return m_testResult; } diff --git a/src/Microsoft.Management.Configuration/TestConfigurationSetResult.h b/src/Microsoft.Management.Configuration/TestConfigurationSetResult.h @@ -15,8 +15,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation void TestResult(ConfigurationTestResult value); #endif - Windows::Foundation::Collections::IVectorView<TestConfigurationUnitResult> UnitResults(); - ConfigurationTestResult TestResult(); + Windows::Foundation::Collections::IVectorView<TestConfigurationUnitResult> UnitResults() const; + ConfigurationTestResult TestResult() const; #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: