From 3d4889dbda107773add7d4a7f6e52a6f5cfae3a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 11:19:17 +0000 Subject: [PATCH 1/3] Initial plan From e01e44009129c4940c4b5d6ecf41fee0fa0d6657 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 11:25:40 +0000 Subject: [PATCH 2/3] Add LoggingUseCMTrace setting with CMTrace log format support and tests Agent-Logs-Url: https://github.com/devicie/winget-cli/sessions/c4d184c0-b0f2-46bc-9e42-e68d558e7f8a Co-authored-by: pl4nty <21111317+pl4nty@users.noreply.github.com> --- src/AppInstallerCLITests/UserSettings.cpp | 40 +++++++++++++ src/AppInstallerCommonCore/FileLogger.cpp | 57 ++++++++++++++++++- .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/UserSettings.cpp | 1 + 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index b1b28519b3..3bb213d957 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -925,6 +925,46 @@ TEST_CASE("SettingOutputSortDirection", "[settings]") } } +TEST_CASE("SettingLoggingUseCMTrace", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Default value") + { + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == false); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Enabled") + { + std::string_view json = R"({ "logging": { "useCMTrace": true } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == true); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Disabled") + { + std::string_view json = R"({ "logging": { "useCMTrace": false } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == false); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Bad value type") + { + std::string_view json = R"({ "logging": { "useCMTrace": "yes" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == false); + REQUIRE(userSettingTest.GetWarnings().size() == 1); + } +} + TEST_CASE("ConvertToSortField", "[settings]") { SECTION("Valid values - lowercase") diff --git a/src/AppInstallerCommonCore/FileLogger.cpp b/src/AppInstallerCommonCore/FileLogger.cpp index a430694ef6..14751b8909 100644 --- a/src/AppInstallerCommonCore/FileLogger.cpp +++ b/src/AppInstallerCommonCore/FileLogger.cpp @@ -29,6 +29,53 @@ namespace AppInstaller::Logging return std::move(strstr).str(); } + // Formats a log line in CMTrace format. + // CMTrace log format: + std::string ToCMTraceLogLine(Channel channel, Level level, std::string_view message) + { + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + tm localTime{}; + _localtime64_s(&localTime, &tt); + + auto sinceEpoch = now.time_since_epoch(); + auto leftoverMillis = std::chrono::duration_cast(sinceEpoch) - std::chrono::duration_cast(sinceEpoch); + + // Get UTC bias in minutes (positive means west of UTC, CMTrace uses positive for west) + long timezoneBiasSeconds = 0; + _get_timezone(&timezoneBiasSeconds); + long biasMins = timezoneBiasSeconds / 60; + + // CMTrace type: 1=Info/Verbose, 2=Warning, 3=Error/Critical + int type; + switch (level) + { + case Level::Warning: type = 2; break; + case Level::Error: + case Level::Crit: type = 3; break; + default: type = 1; break; + } + + std::stringstream strstr; + strstr << "" + << ""; + return std::move(strstr).str(); + } + // Determines the difference between the given position and the maximum as an offset. std::ofstream::off_type CalculateDiff(const std::ofstream::pos_type& position, std::ofstream::off_type maximum) { @@ -94,7 +141,15 @@ namespace AppInstaller::Logging void FileLogger::Write(Channel channel, Level level, std::string_view message) noexcept try { - std::string log = ToLogLine(channel, level, message); + std::string log; + if (Settings::User().Get()) + { + log = ToCMTraceLogLine(channel, level, message); + } + else + { + log = ToLogLine(channel, level, message); + } WriteDirect(channel, level, log); } catch (...) {} diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 9909d9acd3..fe82dd294d 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -133,6 +133,7 @@ namespace AppInstaller::Settings LoggingFileTotalSizeLimitInMB, LoggingFileIndividualSizeLimitInMB, LoggingFileCountLimit, + LoggingUseCMTrace, // Uninstall behavior UninstallPurgePortablePackage, // Download behavior @@ -237,6 +238,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFileTotalSizeLimitInMB, uint32_t, uint32_t, 128, ".logging.file.totalSizeLimitInMB"sv); SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFileIndividualSizeLimitInMB, uint32_t, uint32_t, 16, ".logging.file.individualSizeLimitInMB"sv); SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFileCountLimit, uint32_t, uint32_t, 0, ".logging.file.countLimit"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::LoggingUseCMTrace, bool, bool, false, ".logging.useCMTrace"sv); // Interactivity SETTINGMAPPING_SPECIALIZATION(Setting::InteractivityDisable, bool, bool, false, ".interactivity.disable"sv); // Output behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 38cdd0847a..203e2a216c 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -299,6 +299,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(LoggingFileTotalSizeLimitInMB) WINGET_VALIDATE_PASS_THROUGH(LoggingFileIndividualSizeLimitInMB) WINGET_VALIDATE_PASS_THROUGH(LoggingFileCountLimit) + WINGET_VALIDATE_PASS_THROUGH(LoggingUseCMTrace) #ifndef AICLI_DISABLE_TEST_HOOKS WINGET_VALIDATE_PASS_THROUGH(EnableSelfInitiatedMinidump) From 9d83250e3a78b5709c30d8699d7fa7cca5d72db6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 07:09:13 +0000 Subject: [PATCH 3/3] Rename CMTrace to CCM, change bool setting to logging.format enum, add build workflow Agent-Logs-Url: https://github.com/devicie/winget-cli/sessions/7d39e80e-abcc-4238-8301-b2a0f059dcaf Co-authored-by: pl4nty <21111317+pl4nty@users.noreply.github.com> --- .github/workflows/build-msix.yml | 75 +++++++++++++++++++ src/AppInstallerCLITests/UserSettings.cpp | 38 +++++++--- src/AppInstallerCommonCore/FileLogger.cpp | 12 +-- .../Public/winget/UserSettings.h | 4 +- src/AppInstallerCommonCore/UserSettings.cpp | 17 ++++- .../Public/AppInstallerLogging.h | 9 +++ 6 files changed, 136 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/build-msix.yml diff --git a/.github/workflows/build-msix.yml b/.github/workflows/build-msix.yml new file mode 100644 index 0000000000..94220a9bd2 --- /dev/null +++ b/.github/workflows/build-msix.yml @@ -0,0 +1,75 @@ +name: Build MSIX + +on: + workflow_dispatch: + push: + branches: + - 'copilot/**' + paths: + - 'src/**' + - '.github/workflows/build-msix.yml' + pull_request: + paths: + - 'src/**' + - '.github/workflows/build-msix.yml' + +jobs: + build: + runs-on: windows-latest + timeout-minutes: 120 + + permissions: + contents: read + + strategy: + matrix: + platform: [x64] + configuration: [Release] + + env: + solution: 'src\AppInstallerCLI.sln' + buildOutDir: ${{ github.workspace }}\src\${{ matrix.platform }}\${{ matrix.configuration }} + appxPackageDir: ${{ github.workspace }}\artifacts\${{ matrix.platform }}\AppxPackages + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install NuGet + uses: NuGet/setup-nuget@v2 + + - name: Restore Solution (NuGet) + run: nuget restore ${{ env.solution }} + + - name: Restore AppInstallerCLIPackage (NuGet) + run: nuget restore src\AppInstallerCLIPackage\AppInstallerCLIPackage.wapproj + + - name: Restore .NET projects + run: dotnet restore src + + - name: Integrate vcpkg + run: | + & "$env:VCPKG_INSTALLATION_ROOT\vcpkg.exe" integrate install + shell: pwsh + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build Solution + run: | + msbuild ${{ env.solution }} ` + /p:Platform=${{ matrix.platform }} ` + /p:Configuration=${{ matrix.configuration }} ` + /p:AppxBundlePlatforms="${{ matrix.platform }}" ` + /p:AppxPackageDir="${{ env.appxPackageDir }}" ` + /p:AppxBundle=Always ` + /p:UapAppxPackageBuildMode=SideloadOnly ` + /m + shell: pwsh + + - name: Upload MSIX artifacts + uses: actions/upload-artifact@v4 + with: + name: msix-${{ matrix.platform }}-${{ matrix.configuration }} + path: ${{ env.appxPackageDir }} + retention-days: 7 diff --git a/src/AppInstallerCLITests/UserSettings.cpp b/src/AppInstallerCLITests/UserSettings.cpp index 3bb213d957..1063a810d3 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -925,7 +925,7 @@ TEST_CASE("SettingOutputSortDirection", "[settings]") } } -TEST_CASE("SettingLoggingUseCMTrace", "[settings]") +TEST_CASE("SettingLoggingFormat", "[settings]") { auto again = DeleteUserSettingsFiles(); @@ -933,34 +933,52 @@ TEST_CASE("SettingLoggingUseCMTrace", "[settings]") { UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == false); + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); REQUIRE(userSettingTest.GetWarnings().size() == 0); } - SECTION("Enabled") + SECTION("WinGet") { - std::string_view json = R"({ "logging": { "useCMTrace": true } })"; + std::string_view json = R"({ "logging": { "format": "winget" } })"; SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == true); + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); REQUIRE(userSettingTest.GetWarnings().size() == 0); } - SECTION("Disabled") + SECTION("CCM") { - std::string_view json = R"({ "logging": { "useCMTrace": false } })"; + std::string_view json = R"({ "logging": { "format": "ccm" } })"; SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == false); + REQUIRE(userSettingTest.Get() == LogFileFormat::CCM); REQUIRE(userSettingTest.GetWarnings().size() == 0); } + SECTION("Case insensitive CCM") + { + std::string_view json = R"({ "logging": { "format": "CCM" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == LogFileFormat::CCM); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("Bad value") + { + std::string_view json = R"({ "logging": { "format": "cmtrace" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); + REQUIRE(userSettingTest.GetWarnings().size() == 1); + } SECTION("Bad value type") { - std::string_view json = R"({ "logging": { "useCMTrace": "yes" } })"; + std::string_view json = R"({ "logging": { "format": true } })"; SetSetting(Stream::PrimaryUserSettings, json); UserSettingsTest userSettingTest; - REQUIRE(userSettingTest.Get() == false); + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); REQUIRE(userSettingTest.GetWarnings().size() == 1); } } diff --git a/src/AppInstallerCommonCore/FileLogger.cpp b/src/AppInstallerCommonCore/FileLogger.cpp index 14751b8909..aefd8f97d9 100644 --- a/src/AppInstallerCommonCore/FileLogger.cpp +++ b/src/AppInstallerCommonCore/FileLogger.cpp @@ -29,9 +29,9 @@ namespace AppInstaller::Logging return std::move(strstr).str(); } - // Formats a log line in CMTrace format. - // CMTrace log format: - std::string ToCMTraceLogLine(Channel channel, Level level, std::string_view message) + // Formats a log line in CCM (CMTrace-compatible) format. + // CCM log format: + std::string ToCCMLogLine(Channel channel, Level level, std::string_view message) { auto now = std::chrono::system_clock::now(); auto tt = std::chrono::system_clock::to_time_t(now); @@ -46,7 +46,7 @@ namespace AppInstaller::Logging _get_timezone(&timezoneBiasSeconds); long biasMins = timezoneBiasSeconds / 60; - // CMTrace type: 1=Info/Verbose, 2=Warning, 3=Error/Critical + // CCM type: 1=Info/Verbose, 2=Warning, 3=Error/Critical int type; switch (level) { @@ -142,9 +142,9 @@ namespace AppInstaller::Logging void FileLogger::Write(Channel channel, Level level, std::string_view message) noexcept try { std::string log; - if (Settings::User().Get()) + if (Settings::User().Get() == LogFileFormat::CCM) { - log = ToCMTraceLogLine(channel, level, message); + log = ToCCMLogLine(channel, level, message); } else { diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index fe82dd294d..2761367192 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -133,7 +133,7 @@ namespace AppInstaller::Settings LoggingFileTotalSizeLimitInMB, LoggingFileIndividualSizeLimitInMB, LoggingFileCountLimit, - LoggingUseCMTrace, + LoggingFormat, // Uninstall behavior UninstallPurgePortablePackage, // Download behavior @@ -238,7 +238,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFileTotalSizeLimitInMB, uint32_t, uint32_t, 128, ".logging.file.totalSizeLimitInMB"sv); SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFileIndividualSizeLimitInMB, uint32_t, uint32_t, 16, ".logging.file.individualSizeLimitInMB"sv); SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFileCountLimit, uint32_t, uint32_t, 0, ".logging.file.countLimit"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::LoggingUseCMTrace, bool, bool, false, ".logging.useCMTrace"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::LoggingFormat, std::string, Logging::LogFileFormat, Logging::LogFileFormat::WinGet, ".logging.format"sv); // Interactivity SETTINGMAPPING_SPECIALIZATION(Setting::InteractivityDisable, bool, bool, false, ".interactivity.disable"sv); // Output behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index 203e2a216c..7c3272abe5 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -299,7 +299,6 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(LoggingFileTotalSizeLimitInMB) WINGET_VALIDATE_PASS_THROUGH(LoggingFileIndividualSizeLimitInMB) WINGET_VALIDATE_PASS_THROUGH(LoggingFileCountLimit) - WINGET_VALIDATE_PASS_THROUGH(LoggingUseCMTrace) #ifndef AICLI_DISABLE_TEST_HOOKS WINGET_VALIDATE_PASS_THROUGH(EnableSelfInitiatedMinidump) @@ -531,6 +530,22 @@ namespace AppInstaller::Settings return value * 24h; } + WINGET_VALIDATE_SIGNATURE(LoggingFormat) + { + static constexpr std::string_view s_format_winget = "winget"; + static constexpr std::string_view s_format_ccm = "ccm"; + + if (Utility::CaseInsensitiveEquals(value, s_format_winget)) + { + return LogFileFormat::WinGet; + } + else if (Utility::CaseInsensitiveEquals(value, s_format_ccm)) + { + return LogFileFormat::CCM; + } + return {}; + } + WINGET_VALIDATE_SIGNATURE(OutputSortOrder) { std::vector fields; diff --git a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h index 39dbed0b6f..f7094e942e 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerLogging.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerLogging.h @@ -98,6 +98,15 @@ namespace AppInstaller::Logging ShortGuid, }; + // The format used when writing log entries to a file. + enum class LogFileFormat + { + // Default WinGet format: " [channel] message" + WinGet, + // CCM/CMTrace-compatible format recognized by CMTrace and Microsoft Endpoint Configuration Manager log viewers + CCM, + }; + // Indicates a location of significance in the logging stream. enum class Tag {