Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ fdw
fdwgp
FECAFEB
fedorapeople
fff

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fff

fileinuse
filemode
Filetime
Expand Down
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
24 changes: 24 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: "Copilot Setup Steps"

# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml

jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: windows-latest

# Set the permissions to the lowest permissions possible needed for your steps.
# Copilot will be given its own token for its operations.
permissions:
# If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission.
# If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete.
contents: read
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 @@ namespace AppInstaller::Logging
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="">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// CCM log format: <![LOG[message]LOG]!><time="HH:mm:ss.fff+###" date="MM-dd-YYYY" component="channel" context="" type="N" thread="TID" file="">

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 @@ 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<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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CCM/CMTrace-compatible format: "format syntax here"

CCM,
};

// Indicates a location of significance in the logging stream.
enum class Tag
{
Expand Down
Loading