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 b1b28519b3..1063a810d3 100644 --- a/src/AppInstallerCLITests/UserSettings.cpp +++ b/src/AppInstallerCLITests/UserSettings.cpp @@ -925,6 +925,64 @@ TEST_CASE("SettingOutputSortDirection", "[settings]") } } +TEST_CASE("SettingLoggingFormat", "[settings]") +{ + auto again = DeleteUserSettingsFiles(); + + SECTION("Default value") + { + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("WinGet") + { + std::string_view json = R"({ "logging": { "format": "winget" } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); + REQUIRE(userSettingTest.GetWarnings().size() == 0); + } + SECTION("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("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": { "format": true } })"; + SetSetting(Stream::PrimaryUserSettings, json); + UserSettingsTest userSettingTest; + + REQUIRE(userSettingTest.Get() == LogFileFormat::WinGet); + 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..aefd8f97d9 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 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); + 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; + + // CCM 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() == LogFileFormat::CCM) + { + log = ToCCMLogLine(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..2761367192 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, + LoggingFormat, // 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::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 38cdd0847a..7c3272abe5 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -530,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 {