Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/build-msix.yml
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions src/AppInstallerCLITests/UserSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,64 @@ TEST_CASE("SettingOutputSortDirection", "[settings]")
}
}

TEST_CASE("SettingLoggingFormat", "[settings]")
{
auto again = DeleteUserSettingsFiles();

SECTION("Default value")
{
UserSettingsTest userSettingTest;

REQUIRE(userSettingTest.Get<Setting::LoggingFormat>() == 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<Setting::LoggingFormat>() == 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<Setting::LoggingFormat>() == 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<Setting::LoggingFormat>() == 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<Setting::LoggingFormat>() == 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<Setting::LoggingFormat>() == LogFileFormat::WinGet);
REQUIRE(userSettingTest.GetWarnings().size() == 1);
}
}

TEST_CASE("ConvertToSortField", "[settings]")
{
SECTION("Valid values - lowercase")
Expand Down
57 changes: 56 additions & 1 deletion src/AppInstallerCommonCore/FileLogger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,53 @@
return std::move(strstr).str();
}

// Formats a log line in CCM (CMTrace-compatible) format.
// CCM log format: <![LOG[message]LOG]!><time="HH:mm:ss.fff+###" date="MM-dd-YYYY" component="channel" context="" type="N" thread="TID" file="">

Check failure on line 33 in src/AppInstallerCommonCore/FileLogger.cpp

View workflow job for this annotation

GitHub Actions / Check Spelling

`fff` is not a recognized word (unrecognized-spelling)
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<std::chrono::milliseconds>(sinceEpoch) - std::chrono::duration_cast<std::chrono::seconds>(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 << "<![LOG[" << message << "]LOG]!>"
<< "<time=\""
<< std::setw(2) << std::setfill('0') << localTime.tm_hour << ":"
<< std::setw(2) << std::setfill('0') << localTime.tm_min << ":"
<< std::setw(2) << std::setfill('0') << localTime.tm_sec << "."
<< std::setw(3) << std::setfill('0') << leftoverMillis.count()
<< "+" << biasMins << "\""
<< " date=\""
<< std::setw(2) << std::setfill('0') << (1 + localTime.tm_mon) << "-"
<< std::setw(2) << std::setfill('0') << localTime.tm_mday << "-"
<< (1900 + localTime.tm_year) << "\""
<< " component=\"" << GetChannelName(channel) << "\""
<< " context=\"\""
<< " type=\"" << type << "\""
<< " thread=\"" << GetCurrentThreadId() << "\""
<< " file=\"\">";
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)
{
Expand Down Expand Up @@ -94,7 +141,15 @@

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<Settings::Setting::LoggingFormat>() == LogFileFormat::CCM)
{
log = ToCCMLogLine(channel, level, message);
}
else
{
log = ToLogLine(channel, level, message);
}
WriteDirect(channel, level, log);
}
catch (...) {}
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCommonCore/Public/winget/UserSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ namespace AppInstaller::Settings
LoggingFileTotalSizeLimitInMB,
LoggingFileIndividualSizeLimitInMB,
LoggingFileCountLimit,
LoggingFormat,
// Uninstall behavior
UninstallPurgePortablePackage,
// Download behavior
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/AppInstallerCommonCore/UserSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<SortField> fields;
Expand Down
9 changes: 9 additions & 0 deletions src/AppInstallerSharedLib/Public/AppInstallerLogging.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ namespace AppInstaller::Logging
ShortGuid,
};

// The format used when writing log entries to a file.
enum class LogFileFormat
{
// Default WinGet format: "<timestamp> <level> [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
{
Expand Down
Loading