Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export command for winget settings. #2719

Merged
merged 11 commits into from
Dec 13, 2022
44 changes: 37 additions & 7 deletions src/AppInstallerCLICore/Commands/SettingsCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ namespace AppInstaller::CLI
constexpr Utility::LocIndView s_ArgumentName_Enable = "enable"_liv;
constexpr Utility::LocIndView s_ArgumentName_Disable = "disable"_liv;
constexpr Utility::LocIndView s_ArgName_EnableAndDisable = "enable|disable"_liv;
static constexpr std::string_view s_SettingsCommand_HelpLink = "https://aka.ms/winget-settings"sv;
}

std::vector<Argument> SettingsCommand::GetArguments() const
{
return {
Argument{ s_ArgumentName_Enable, Argument::NoAlias, Execution::Args::Type::AdminSettingEnable, Resource::String::AdminSettingEnableDescription, ArgumentType::Standard, Argument::Visibility::Help },
Argument{ s_ArgumentName_Disable, Argument::NoAlias, Execution::Args::Type::AdminSettingDisable, Resource::String::AdminSettingDisableDescription, ArgumentType::Standard, Argument::Visibility::Help },
};
std::vector<std::unique_ptr<Command>> SettingsCommand::GetCommands() const
{
return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({
std::make_unique<SettingsExportCommand>(FullName()),
});
}

std::vector<Argument> SettingsCommand::GetArguments() const
{
return {
Argument{ s_ArgumentName_Enable, Argument::NoAlias, Execution::Args::Type::AdminSettingEnable, Resource::String::AdminSettingEnableDescription, ArgumentType::Standard, Argument::Visibility::Help },
Argument{ s_ArgumentName_Disable, Argument::NoAlias, Execution::Args::Type::AdminSettingDisable, Resource::String::AdminSettingDisableDescription, ArgumentType::Standard, Argument::Visibility::Help },
};
}

Resource::LocString SettingsCommand::ShortDescription() const
Expand All @@ -38,7 +46,7 @@ namespace AppInstaller::CLI

std::string SettingsCommand::HelpLink() const
{
return "https://aka.ms/winget-settings";
return std::string{ s_SettingsCommand_HelpLink };
}

void SettingsCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const
Expand Down Expand Up @@ -79,4 +87,26 @@ namespace AppInstaller::CLI
context << Workflow::OpenUserSetting;
}
}

Resource::LocString SettingsExportCommand::ShortDescription() const
{
return { Resource::String::SettingsExportCommandShortDescription };
}

Resource::LocString SettingsExportCommand::LongDescription() const
{
return { Resource::String::SettingsExportCommandLongDescription };
}

std::string SettingsExportCommand::HelpLink() const
{
return std::string{ s_SettingsCommand_HelpLink };
}

void SettingsExportCommand::ExecuteInternal(Execution::Context& context) const
{
context <<
Workflow::EnsureRunningAsAdmin <<
Copy link
Contributor

Choose a reason for hiding this comment

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

Workflow::EnsureRunningAsAdmin

Just curious is there a particular reason to require admin for export? Exporting seems harmless to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's no real reason. Removing.

Workflow::ExportAdminSettings;
}
}
14 changes: 14 additions & 0 deletions src/AppInstallerCLICore/Commands/SettingsCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace AppInstaller::CLI
{
SettingsCommand(std::string_view parent) : Command("settings", { "config" }, parent, Settings::TogglePolicy::Policy::Settings) {}

std::vector<std::unique_ptr<Command>> GetCommands() const override;
std::vector<Argument> GetArguments() const override;

virtual Resource::LocString ShortDescription() const override;
Expand All @@ -20,4 +21,17 @@ namespace AppInstaller::CLI
void ValidateArgumentsInternal(Execution::Args& execArgs) const override;
void ExecuteInternal(Execution::Context& context) const override;
};

struct SettingsExportCommand final : public Command
{
SettingsExportCommand(std::string_view parent) : Command("export", parent) {}

Resource::LocString ShortDescription() const override;
Resource::LocString LongDescription() const override;

std::string HelpLink() const override;

protected:
void ExecuteInternal(Execution::Context& context) const override;
};
}
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(SettingLoadFailure);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsExportCommandLongDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsExportCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningField);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarnings);
WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningValue);
Expand Down
45 changes: 45 additions & 0 deletions src/AppInstallerCLICore/Workflows/SettingsFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ namespace AppInstaller::CLI::Workflow
using namespace AppInstaller::Settings;
using namespace AppInstaller::Utility;

namespace
{
struct AdminSettings
{
AdminSettings()
{
root["adminSettings"] = Json::ValueType::objectValue;
Copy link
Contributor

Choose a reason for hiding this comment

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

root["adminSettings"] = Json::ValueType::objectValue;

Is the exported settings to be imported in Powershell DSC? Do we need a schema version or it's unlikely to change in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For DSC... not entirely. The admin settings resource will take a hashtable with something really similar to value of the adminSettings. We just needed a way to expose it without reading registry keys.

Also, because reasons (mostly testing and not having to special case powershell) I'm adding the path of the local user settings file to winget settings export . Also winget --info for completeness.

But yes, I'm going to add a new schema.

}

void Add(AdminSetting setting)
{
auto str = std::string{ Settings::AdminSettingToString(setting) };
root["adminSettings"][str] = Settings::IsAdminSettingEnabled(setting);
}

std::string ToJsonString() const
{
Json::StreamWriterBuilder writerBuilder;
writerBuilder.settings_["indentation"] = "";
return Json::writeString(writerBuilder, root);
}

private:
Json::Value root{ Json::ValueType::objectValue };
};
}

void EnableAdminSetting(Execution::Context& context)
{
std::string_view adminSettingString = context.Args.GetArg(Execution::Args::Type::AdminSettingEnable);
Expand Down Expand Up @@ -99,4 +126,22 @@ namespace AppInstaller::CLI::Workflow
ShellExecuteW(nullptr, nullptr, L"notepad", filePathUTF16.c_str(), nullptr, SW_SHOW);
}
}

void ExportAdminSettings(Execution::Context& context)
{
AdminSetting settings[] =
Copy link
Member

Choose a reason for hiding this comment

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

This would be better if there were a Max enum value and we could just enumerate for (i = firstValidSetting; i < AdminSetting::Max; ++i) so that any future settings are automatically included.

{
AdminSetting::LocalManifestFiles,
AdminSetting::BypassCertificatePinningForMicrosoftStore
};

AdminSettings adminSettings;

for (auto& setting : settings)
{
adminSettings.Add(setting);
}

context.Reporter.Info() << adminSettings.ToJsonString() << std::endl;
}
}
6 changes: 6 additions & 0 deletions src/AppInstallerCLICore/Workflows/SettingsFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ namespace AppInstaller::CLI::Workflow
// Inputs: None
// Outputs: None
void OpenUserSetting(Execution::Context& context);

// Lists the state of admin settings.
// Required Args: None
// Inputs: None
// Outputs: None
void ExportAdminSettings(Execution::Context& context);
}
6 changes: 6 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -1483,4 +1483,10 @@ Please specify one of them using the `--source` option to proceed.</value>
<value>Cannot disable %1. This setting is controlled by policy. For more information contact your system administrator.</value>
<comment>{Locked="%1"} The value will be replaced with the feature name</comment>
</data>
<data name="SettingsExportCommandLongDescription" xml:space="preserve">
<value>Export administrator settings as JSON</value>
msftrubengu marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="SettingsExportCommandShortDescription" xml:space="preserve">
<value>Export administrator settings</value>
</data>
</root>
33 changes: 33 additions & 0 deletions src/AppInstallerCLITests/TestCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,37 @@ namespace TestCommon
return AppInstaller::Msix::GetPackageReader(stream.Get(), &packageReader)
&& SUCCEEDED(packageReader->GetManifest(manifestReader));
}

std::string RemoveConsoleFormat(const std::string& str)
{
// We are looking something in the lines of "\x1b[0mthisisastring"
if (!str.empty() && str[0] == '\x1b')
{
// Find first m
auto pos = str.find("m");
if (pos != std::string::npos)
{
return str.substr(pos + 1);
}
}

return str;
}

Json::Value ConvertToJson(const std::string& content)
{
auto contentClean = RemoveConsoleFormat(content);

Json::Value root;
Json::CharReaderBuilder builder;
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
std::string error;

if (!reader->parse(contentClean.c_str(), contentClean.c_str() + contentClean.size(), &root, &error))
{
throw error;
}

return root;
}
}
6 changes: 6 additions & 0 deletions src/AppInstallerCLITests/TestCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,10 @@ namespace TestCommon

// Get manifest reader from a msix file path
bool GetMsixPackageManifestReader(std::string_view testFileName, IAppxManifestReader** manifestReader);

// Removes console format
std::string RemoveConsoleFormat(const std::string& str);

// Convert to Json::Value
Json::Value ConvertToJson(const std::string& content);
}
42 changes: 41 additions & 1 deletion src/AppInstallerCLITests/WorkFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3724,4 +3724,44 @@ TEST_CASE("InstallFlow_FoundInstalledAndUpgradeNotAvailable", "[UpdateFlow][work
REQUIRE(!std::filesystem::exists(installResultPath.GetPath()));
REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::UpdateNotApplicable).get()) != std::string::npos);
REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE);
}
}

TEST_CASE("Export_Settings", "[Settings][workflow]")
{
RemoveSetting(Stream::AdminSettings);

{
// No admin settings, local manifest should be false.
std::ostringstream exportOutput;
TestContext context{ exportOutput, std::cin };
auto previousThreadGlobals = context.SetForCurrentThread();
context.Override({ EnsureRunningAsAdmin, [](TestContext&) {} });
SettingsExportCommand settingsExportCommand({});
settingsExportCommand.Execute(context);

auto json = ConvertToJson(exportOutput.str());
REQUIRE(!json.isNull());
REQUIRE_FALSE(json["adminSettings"]["LocalManifestFiles"].asBool());
}

{
// Enable local manifest and verify export works.
std::ostringstream settingsOutput;
TestContext context{ settingsOutput, std::cin };
auto previousThreadGlobals = context.SetForCurrentThread();
context.Args.AddArg(Execution::Args::Type::AdminSettingEnable, "LocalManifestFiles"sv);
context.Override({ EnsureRunningAsAdmin, [](TestContext&) {} });
SettingsCommand settings({});
settings.Execute(context);

std::ostringstream exportOutput;
TestContext context2{ exportOutput, std::cin };
auto previousThreadGlobals2 = context2.SetForCurrentThread();
context2.Override({ EnsureRunningAsAdmin, [](TestContext&) {} });
SettingsExportCommand settingsExportCommand({});
settingsExportCommand.Execute(context2);
auto json = ConvertToJson(exportOutput.str());
REQUIRE(!json.isNull());
REQUIRE(json["adminSettings"]["LocalManifestFiles"].asBool());
}
}
2 changes: 2 additions & 0 deletions src/AppInstallerCLITests/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

#include <catch.hpp>

#include <json/json.h>

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Globalization.h>
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCommonCore/Public/winget/AdminSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace AppInstaller::Settings
{
// Enum of admin settings.
// When a new one is added, it should be also added to SettingsCommand::ExportAdminSettings
enum class AdminSetting
{
Unknown,
Expand Down