commit 4f05a11cebd8d9255924711c7ef233d87685d03d parent 0b3ad7fd283ceb22969a60eea8c84692cad72bff Author: JohnMcPMS <johnmcp@microsoft.com> Date: Tue, 10 Nov 2020 09:40:16 -0800 Add ARP (Add/Remove Programs) data to list (#633) With this change, the `list` command will now show packages from the machine and user Add\Remove Programs list. This is done by reading from the registry locations that drive that UI, for both scopes and all appropriate architectures. New common code is added to enable enumeration of registry keys and reading of values. This is then leveraged in a set of helper functions to enable the existing installed package source to read in the relevant data. It populates the index, and includes metadata relevant to `upgrade` and `uninstall`. Entries marked as `SystemComponent` are excluded, as are those that do not have a `DisplayName`. If a version is not discernable, it is marked as "Unknown". This will lead it to be considered for potential upgrades until we properly track everything that winget installs and can also associate with the system data. Diffstat:
47 files changed, 1809 insertions(+), 135 deletions(-)
diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -20,7 +20,7 @@ namespace AppInstaller::CLI::Execution { -#define WINGET_OSTREAM_FORMAT_HRESULT(hr) "0x" << std::hex << std::setw(8) << std::setfill('0') << hr +#define WINGET_OSTREAM_FORMAT_HRESULT(hr) "0x" << Logging::SetHRFormat << hr // Reporter should be the central place to show workflow status to user. struct Reporter : public IProgressSink diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -125,7 +125,12 @@ namespace AppInstaller::CLI::Workflow return {}; } - Logging::Telemetry().LogSelectedInstaller(static_cast<int>(result->Arch), result->Url, Manifest::ManifestInstaller::InstallerTypeToString(result->InstallerType), result->Scope, result->Language); + Logging::Telemetry().LogSelectedInstaller( + static_cast<int>(result->Arch), + result->Url, + Manifest::ManifestInstaller::InstallerTypeToString(result->InstallerType), + Manifest::ManifestInstaller::ScopeToString(result->Scope), + result->Language); return *result; } diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -15,7 +15,7 @@ namespace AppInstaller::CLI::Workflow // ShellExecutes the given path. std::optional<DWORD> InvokeShellExecute(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress) { - AICLI_LOG(CLI, Info, << "Starting installer. Path: " << filePath); + AICLI_LOG(CLI, Info, << "Starting installer: '" << filePath.u8string() << "' with arguments '" << args << '\''); SHELLEXECUTEINFOW execInfo = { 0 }; execInfo.cbSize = sizeof(execInfo); diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -3,19 +3,26 @@ namespace AppInstallerCLIE2ETests { - using NUnit.Framework; + using System; + using System.IO; using System.Threading; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using NUnit.Framework; public class BaseCommand { + public readonly string SettingsJsonFilePath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json"; + public readonly string LocalAppData = "LocalAppData"; + [OneTimeSetUp] - public void Setup() + public void BaseSetup() { ResetTestSource(); } [OneTimeTearDown] - public void Teardown() + public void BaseTeardown() { TestCommon.RunAICLICommand("source reset", "--force"); } @@ -27,5 +34,35 @@ namespace AppInstallerCLIE2ETests TestCommon.RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl}"); Thread.Sleep(5000); } + + public void ConfigureFeature(string featureName, bool status) + { + string localAppDataPath = Environment.GetEnvironmentVariable(LocalAppData); + JObject settingsJson = JObject.Parse(File.ReadAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath))); + JObject experimentalFeatures = (JObject)settingsJson["experimentalFeatures"]; + experimentalFeatures[featureName] = status; + + File.WriteAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath), settingsJson.ToString()); + } + + public void InitializeAllFeatures(bool status) + { + string localAppDataPath = Environment.GetEnvironmentVariable(LocalAppData); + + var settingsJson = new + { + experimentalFeatures = new + { + experimentalArg = status, + experimentalCmd = status, + experimentalMSStore = status, + list = status, + upgrade = status + } + }; + + var serializedSettingsJson = JsonConvert.SerializeObject(settingsJson, Formatting.Indented); + File.WriteAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath), serializedSettingsJson); + } } } diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -9,11 +9,8 @@ namespace AppInstallerCLIE2ETests using Newtonsoft.Json.Linq; using NUnit.Framework; - public class FeaturesCommand + public class FeaturesCommand : BaseCommand { - private const string SettingsJsonFilePath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json"; - private const string LocalAppData = "LocalAppData"; - [SetUp] public void Setup() { @@ -46,33 +43,5 @@ namespace AppInstallerCLIE2ETests var result = TestCommon.RunAICLICommand("features", ""); Assert.True(result.StdOut.Contains("Enabled")); } - - private void ConfigureFeature(string featureName, bool status) - { - string localAppDataPath = Environment.GetEnvironmentVariable(LocalAppData); - JObject settingsJson = JObject.Parse(File.ReadAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath))); - JObject experimentalFeatures = (JObject)settingsJson["experimentalFeatures"]; - experimentalFeatures[featureName] = status; - - File.WriteAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath), settingsJson.ToString()); - } - - private void InitializeAllFeatures(bool status) - { - string localAppDataPath = Environment.GetEnvironmentVariable(LocalAppData); - - var settingsJson = new - { - experimentalFeatures = new - { - experimentalArg = status, - experimentalCmd = status, - experimentalMSStore = status - } - }; - - var serializedSettingsJson = JsonConvert.SerializeObject(settingsJson, Formatting.Indented); - File.WriteAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath), serializedSettingsJson); - } } } diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -7,23 +7,51 @@ namespace AppInstallerCLIE2ETests public class ListCommand : BaseCommand { - //[Test] - public void List() + [SetUp] + public void Setup() { - var result = TestCommon.RunAICLICommand("list", ""); + InitializeAllFeatures(false); + ConfigureFeature("list", true); + } + + [TearDown] + public void TearDown() + { + InitializeAllFeatures(false); + } + + [Test] + public void ListSelf() + { + var result = TestCommon.RunAICLICommand("list", Constants.AICLIPackageFamilyName); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("PowerShell")); - Assert.True(result.StdOut.Contains("Microsoft.PowerShell")); + Assert.True(result.StdOut.Contains(Constants.AICLIPackageFamilyName)); } - //[Test] + [Test] public void ListAfterInstall() { + System.Guid guid = System.Guid.NewGuid(); + string productCode = guid.ToString(); var installDir = TestCommon.GetRandomTestDir(); - TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); - var result = TestCommon.RunAICLICommand("list", ""); + + string localAppDataPath = System.Environment.GetEnvironmentVariable(LocalAppData); + string logFilePath = System.IO.Path.Combine(localAppDataPath, Constants.E2ETestLogsPath); + logFilePath = System.IO.Path.Combine(logFilePath, "ListAfterInstall-" + System.IO.Path.GetRandomFileName() + ".log"); + + var result = TestCommon.RunAICLICommand("list", productCode); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + + result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --override \"/InstallDir {installDir} /ProductID {productCode} /LogFile {logFilePath}\""); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + result = TestCommon.RunAICLICommand("list", productCode); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller")); + Assert.True(result.StdOut.Contains(productCode)); + Assert.True(result.StdOut.Contains("1.0.0.0")); + // TODO: Uncomment when install starts tracking packages. Until then this won't be true because our install + // in this test is using a random guid that won't be in the index. + //Assert.True(result.StdOut.Contains("2.0.0.0")); } } } diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -7,6 +7,7 @@ namespace AppInstallerCLIE2ETests using System; using System.Diagnostics; using System.IO; + using System.Text; using System.Threading; public class TestCommon @@ -125,13 +126,16 @@ namespace AppInstallerCLIE2ETests } string workDirectory = GetRandomTestDir(); + string tempBatchFile = Path.Combine(workDirectory, "Batch.cmd"); string exitCodeFile = Path.Combine(workDirectory, "ExitCode.txt"); string stdOutFile = Path.Combine(workDirectory, "StdOut.txt"); string stdErrFile = Path.Combine(workDirectory, "StdErr.txt"); - cmdCommandPiped += $"{AICLIPath} {command} {parameters} > {stdOutFile} 2> {stdErrFile} & call echo %^ERRORLEVEL% > {exitCodeFile}"; + // First change the codepage so that the rest of the batch file works + cmdCommandPiped += $"chcp 65001\n{AICLIPath} {command} {parameters} > {stdOutFile} 2> {stdErrFile}\necho %ERRORLEVEL% > {exitCodeFile}"; + File.WriteAllText(tempBatchFile, cmdCommandPiped, new System.Text.UTF8Encoding(false)); - string psCommand = $"Invoke-CommandInDesktopPackage -PackageFamilyName {Constants.AICLIPackageFamilyName} -AppId {Constants.AICLIAppId} -PreventBreakaway -Command cmd.exe -Args '/c \"{cmdCommandPiped}\"'"; + string psCommand = $"Invoke-CommandInDesktopPackage -PackageFamilyName {Constants.AICLIPackageFamilyName} -AppId {Constants.AICLIAppId} -PreventBreakaway -Command cmd.exe -Args '/c \"{tempBatchFile}\"'"; var psInvokeResult = RunCommandWithResult("powershell", psCommand); diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExeInstaller.2.0.0.0.yaml @@ -0,0 +1,19 @@ +Id: AppInstallerTest.TestExeInstaller +Name: TestExeInstaller +Version: 2.0.0.0 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + Switches: + Custom: /execustom + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -187,6 +187,7 @@ <ClCompile Include="MsixInfo.cpp" /> <ClCompile Include="PredefinedInstalledSource.cpp" /> <ClCompile Include="PreIndexedPackageSource.cpp" /> + <ClCompile Include="Registry.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> <ClCompile Include="TestSource.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -107,6 +107,9 @@ <ClCompile Include="TestSource.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Registry.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/PredefinedInstalledSource.cpp b/src/AppInstallerCLITests/PredefinedInstalledSource.cpp @@ -6,16 +6,132 @@ #include <AppInstallerRuntime.h> #include <AppInstallerStrings.h> #include <Microsoft/PredefinedInstalledSourceFactory.h> +#include <Microsoft/ARPHelper.h> using namespace std::string_literals; using namespace std::string_view_literals; using namespace TestCommon; using namespace AppInstaller; +using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Runtime; using namespace AppInstaller::Utility; +using SQLiteIndex = AppInstaller::Repository::Microsoft::SQLiteIndex; using Factory = AppInstaller::Repository::Microsoft::PredefinedInstalledSourceFactory; +using ARPHelper = AppInstaller::Repository::Microsoft::ARPHelper; + +constexpr std::string_view s_TestScope = "TestScope"sv; + +struct ARPEntry +{ + ARPEntry(std::string entryName) : EntryName(std::move(entryName)) {} + ARPEntry(std::string entryName, std::optional<std::string> displayName, std::optional<std::string> displayVersion, bool systemComponent = false) : + EntryName(std::move(entryName)), DisplayName(std::move(displayName)), DisplayVersion(std::move(displayVersion)), SystemComponent(systemComponent) {} + + std::string EntryName; + std::optional<std::string> DisplayName; + std::optional<std::string> DisplayVersion; + std::optional<std::string> Publisher; + std::optional<std::string> InstallLocation; + std::optional<std::string> UninstallString; + std::optional<std::string> QuietUninstallString; + std::optional<bool> WindowsInstaller; + std::optional<bool> SystemComponent; +}; + +void AddARPValueToKey(HKEY key, const std::wstring& name, const std::optional<std::string>& value) +{ + if (value) + { + SetRegistryValue(key, name, ConvertToUTF16(value.value())); + } +} + +void AddARPValueToKey(HKEY key, const std::wstring& name, const std::optional<bool>& value) +{ + if (value) + { + SetRegistryValue(key, name, (value.value() ? 1 : 0)); + } +} + +void AddARPEntryToKey(HKEY key, const ARPHelper& helper, const ARPEntry& entry) +{ + auto subkey = RegCreateVolatileSubKey(key, ConvertToUTF16(entry.EntryName)); + +#define ADD_ARP_VALUE(_name_) AddARPValueToKey(subkey.get(), helper._name_, entry._name_) + ADD_ARP_VALUE(DisplayName); + ADD_ARP_VALUE(DisplayVersion); + ADD_ARP_VALUE(Publisher); + ADD_ARP_VALUE(InstallLocation); + ADD_ARP_VALUE(UninstallString); + ADD_ARP_VALUE(QuietUninstallString); + ADD_ARP_VALUE(WindowsInstaller); + ADD_ARP_VALUE(SystemComponent); +#undef ADD_ARP_VALUE +} + +void AddARPEntriesToKey(HKEY key, const ARPHelper& helper, const std::vector<ARPEntry>& entries) +{ + for (const auto& entry : entries) + { + AddARPEntryToKey(key, helper, entry); + } +} + +SQLiteIndex::MetadataResult::const_iterator Find(const SQLiteIndex::MetadataResult& metadata, PackageVersionMetadata value) +{ + return std::find_if(metadata.begin(), metadata.end(), [value](const auto& m) { return m.first == value; }); +} + +void VerifyInstalledType(const SQLiteIndex::MetadataResult& metadata, ManifestInstaller::InstallerTypeEnum type) +{ + auto itr = Find(metadata, PackageVersionMetadata::InstalledType); + REQUIRE(itr != metadata.end()); + REQUIRE(ManifestInstaller::ConvertToInstallerTypeEnum(itr->second) == type); +} + +void VerifyTestScope(const SQLiteIndex::MetadataResult& metadata) +{ + auto itr = Find(metadata, PackageVersionMetadata::InstalledScope); + REQUIRE(itr != metadata.end()); + REQUIRE(itr->second == s_TestScope); +} + +void VerifyMetadataString(const SQLiteIndex::MetadataResult& metadata, PackageVersionMetadata pvm, const std::optional<std::string>& value) +{ + auto itr = Find(metadata, pvm); + if (value) + { + REQUIRE(itr != metadata.end()); + REQUIRE(itr->second == value.value()); + } + else + { + REQUIRE(itr == metadata.end()); + } +} + +void VerifyEntryAgainstIndex(const SQLiteIndex& index, SQLiteIndex::IdType manifestId, const ARPEntry& entry) +{ + REQUIRE(index.GetPropertyByManifestId(manifestId, PackageVersionProperty::Id) == entry.EntryName); + REQUIRE(index.GetPropertyByManifestId(manifestId, PackageVersionProperty::Name) == entry.DisplayName); + REQUIRE(index.GetPropertyByManifestId(manifestId, PackageVersionProperty::Version) == entry.DisplayVersion); + + REQUIRE(index.GetMultiPropertyByManifestId(manifestId, PackageVersionMultiProperty::PackageFamilyName).empty()); + auto productCodes = index.GetMultiPropertyByManifestId(manifestId, PackageVersionMultiProperty::ProductCode); + REQUIRE(productCodes.size() == 1); + REQUIRE(productCodes[0] == FoldCase(static_cast<std::string_view>(entry.EntryName))); + + auto metadata = index.GetMetadataByManifestId(manifestId); + + VerifyInstalledType(metadata, entry.WindowsInstaller.value_or(false) ? ManifestInstaller::InstallerTypeEnum::Msi : ManifestInstaller::InstallerTypeEnum::Exe); + VerifyTestScope(metadata); + VerifyMetadataString(metadata, PackageVersionMetadata::InstalledLocation, entry.InstallLocation); + VerifyMetadataString(metadata, PackageVersionMetadata::StandardUninstallCommand, entry.UninstallString); + VerifyMetadataString(metadata, PackageVersionMetadata::SilentUninstallCommand, entry.QuietUninstallString); +} std::shared_ptr<ISource> CreatePredefinedInstalledSource(Factory::Filter filter = Factory::Filter::None) { @@ -29,6 +145,246 @@ std::shared_ptr<ISource> CreatePredefinedInstalledSource(Factory::Filter filter return factory->Create(details, progress); } +TEST_CASE("ARPHelper_GetARPForArchitecture", "[arphelper][list]") +{ + auto systemArch = GetSystemArchitecture(); + + ARPHelper helper; + + auto nativeMachineKey = helper.GetARPKey(ManifestInstaller::ScopeEnum::Machine, systemArch); + REQUIRE(nativeMachineKey); +} + +TEST_CASE("ARPHelper_GetBoolValue_DoesNotExist", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + std::wstring valueName = L"TestValueName"; + + ARPHelper helper; + + REQUIRE_FALSE(helper.GetBoolValue(key, valueName)); +} + +TEST_CASE("ARPHelper_GetBoolValue_NotDword", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + std::wstring valueName = L"TestValueName"; + + SetRegistryValue(root.get(), valueName, L"True"); + + ARPHelper helper; + + REQUIRE_FALSE(helper.GetBoolValue(key, valueName)); +} + +TEST_CASE("ARPHelper_GetBoolValue_Zero", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + std::wstring valueName = L"TestValueName"; + + SetRegistryValue(root.get(), valueName, 0); + + ARPHelper helper; + + REQUIRE_FALSE(helper.GetBoolValue(key, valueName)); +} + +TEST_CASE("ARPHelper_GetBoolValue_One", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + std::wstring valueName = L"TestValueName"; + + SetRegistryValue(root.get(), valueName, 1); + + ARPHelper helper; + + REQUIRE(helper.GetBoolValue(key, valueName)); +} + +TEST_CASE("ARPHelper_GetBoolValue_FortyTwo", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + std::wstring valueName = L"TestValueName"; + + SetRegistryValue(root.get(), valueName, 42); + + ARPHelper helper; + + REQUIRE(helper.GetBoolValue(key, valueName)); +} + +TEST_CASE("ARPHelper_DetermineVersion_DisplayVersion", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + SetRegistryValue(root.get(), helper.DisplayVersion, L"1.0"); + SetRegistryValue(root.get(), helper.Version, 0x0207002A); + SetRegistryValue(root.get(), helper.VersionMajor, 3); + SetRegistryValue(root.get(), helper.VersionMinor, 14); + + auto result = helper.DetermineVersion(key); + REQUIRE(result == "1.0"); +} + +TEST_CASE("ARPHelper_DetermineVersion_Version", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + SetRegistryValue(root.get(), helper.Version, 0x0207002A); + SetRegistryValue(root.get(), helper.VersionMajor, 3); + SetRegistryValue(root.get(), helper.VersionMinor, 14); + + auto result = helper.DetermineVersion(key); + REQUIRE(result == "2.7.42"); +} + +TEST_CASE("ARPHelper_DetermineVersion_VersionMajorMinor", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + SetRegistryValue(root.get(), helper.VersionMajor, 3); + SetRegistryValue(root.get(), helper.VersionMinor, 14); + + auto result = helper.DetermineVersion(key); + REQUIRE(result == "3.14"); +} + +TEST_CASE("ARPHelper_DetermineVersion_Unknown", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + auto result = helper.DetermineVersion(key); + REQUIRE(result == Version::CreateUnknown().ToString()); +} + +TEST_CASE("ARPHelper_PopulateIndexFromKey_Single", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + // Create a single ARP entry under the root + ARPEntry entry("SingleEntry"); + + entry.DisplayName = "Test Name"; + entry.DisplayVersion = "1.2"; + entry.Publisher = "Test Publisher"; + entry.InstallLocation = "TestLocation"; + entry.UninstallString = "Test Uninstall"; + entry.QuietUninstallString = "Test Quiet Uninstall"; + entry.WindowsInstaller = true; + + AddARPEntryToKey(root.get(), helper, entry); + + auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); + + auto result = index.Search({}); + + REQUIRE(result.Matches.size() == 1); + VerifyEntryAgainstIndex(index, result.Matches[0].first, entry); +} + +TEST_CASE("ARPHelper_PopulateIndexFromKey_SingleValid", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + // Create a single ARP entry under the root + ARPEntry entry("SingleEntry"); + + entry.DisplayName = "Test Name"; + entry.DisplayVersion = "1.2"; + entry.Publisher = "Test Publisher"; + entry.InstallLocation = "TestLocation"; + entry.UninstallString = "Test Uninstall"; + entry.QuietUninstallString = "Test Quiet Uninstall"; + entry.WindowsInstaller = false; + + AddARPEntryToKey(root.get(), helper, entry); + + // Name and version must exist, as well as not being a system component. + AddARPEntriesToKey(root.get(), helper, { + { "ValidButIsSystemComponent", "A", "0.1", true }, + { "NoName", {}, "0.2" }, + { "Nothing" }, + }); + + auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); + + auto result = index.Search({}); + + REQUIRE(result.Matches.size() == 1); + VerifyEntryAgainstIndex(index, result.Matches[0].first, entry); +} + +TEST_CASE("ARPHelper_PopulateIndexFromKey_Two", "[arphelper][list]") +{ + auto root = RegCreateVolatileTestRoot(); + Registry::Key key(root.get()); + + ARPHelper helper; + + ARPEntry entry1("FirstEntry"); + entry1.DisplayName = "Test Name"; + entry1.DisplayVersion = "1.2"; + entry1.Publisher = "Test Publisher"; + entry1.InstallLocation = "TestLocation"; + entry1.UninstallString = "Test Uninstall"; + entry1.QuietUninstallString = "Test Quiet Uninstall"; + entry1.WindowsInstaller = true; + + ARPEntry entry2("SecondEntry"); + entry2.DisplayName = "Different Test Name"; + entry2.DisplayVersion = "31.4"; + entry2.Publisher = "Different Test Publisher"; + entry2.InstallLocation = "DifferentTestLocation"; + entry2.UninstallString = "Different Test Uninstall"; + entry2.QuietUninstallString = "Different Test Quiet Uninstall"; + + AddARPEntryToKey(root.get(), helper, entry1); + AddARPEntryToKey(root.get(), helper, entry2); + + auto index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET); + helper.PopulateIndexFromKey(index, key, s_TestScope, "TestArchitecture"); + + REQUIRE(index.Search({}).Matches.size() == 2); + + SearchRequest request; + request.Query = RequestMatch(MatchType::Exact, entry1.EntryName); + auto result = index.Search(request); + + REQUIRE(result.Matches.size() == 1); + VerifyEntryAgainstIndex(index, result.Matches[0].first, entry1); + + request.Query = RequestMatch(MatchType::Exact, entry2.EntryName); + result = index.Search(request); + + REQUIRE(result.Matches.size() == 1); + VerifyEntryAgainstIndex(index, result.Matches[0].first, entry2); +} + TEST_CASE("PredefinedInstalledSource_Create", "[installed][list]") { auto source = CreatePredefinedInstalledSource(); @@ -42,5 +398,5 @@ TEST_CASE("PredefinedInstalledSource_Search", "[installed][list]") auto results = source->Search(request); - REQUIRE(!results.Matches.empty()); + REQUIRE_FALSE(results.Matches.empty()); } diff --git a/src/AppInstallerCLITests/Registry.cpp b/src/AppInstallerCLITests/Registry.cpp @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerStrings.h> +#include <winget/Registry.h> + +using namespace std::string_literals; +using namespace std::string_view_literals; +using namespace AppInstaller::Registry; +using namespace AppInstaller::Utility; +using namespace TestCommon; + +TEST_CASE("EmptyKey", "[registry]") +{ + Key key; + REQUIRE(!key); +} + +TEST_CASE("Constructor_NotFound", "[registry]") +{ + Key key; + REQUIRE_THROWS_HR(key = Key(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Foo\\Bar\\Does\\Not\\Exist"), HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); +} + +TEST_CASE("OpenIfExists_NotFound", "[registry]") +{ + Key key = Key::OpenIfExists(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Foo\\Bar\\Does\\Not\\Exist"); + REQUIRE(!key); +} + +TEST_CASE("EnumerateKeys", "[registry]") +{ + wil::unique_hkey root = RegCreateVolatileTestRoot(); + + std::vector<std::wstring> subKeyNames = { L"A", L"BEE", L"SEE", L"deigh" }; + for (const auto& name : subKeyNames) + { + RegCreateVolatileSubKey(root.get(), name); + } + + Key key{ root.get(), L"" }; + + for (const auto& subkey : key) + { + INFO(subkey.Name()); + + std::wstring nameUtf16 = ConvertToUTF16(subkey.Name()); + + auto itr = std::find(subKeyNames.begin(), subKeyNames.end(), nameUtf16); + if (itr == subKeyNames.end()) + { + FAIL(); + } + else + { + subKeyNames.erase(itr); + } + + Key sk = subkey.Open(); + REQUIRE(sk); + } + + REQUIRE(subKeyNames.empty()); +} + +TEST_CASE("Values_String", "[registry]") +{ + std::wstring valueName = L"TestValueName"; + std::wstring valueValue = L"TestValueValue"; + + wil::unique_hkey root = RegCreateVolatileTestRoot(); + SetRegistryValue(root.get(), valueName, valueValue); + + Key key{ root.get(), L"" }; + + auto value = key[valueName]; + REQUIRE(value); + REQUIRE(value->GetType() == Value::Type::String); + REQUIRE(value->GetValue<Value::Type::String>() == ConvertToUTF8(valueValue)); +} + +TEST_CASE("Values_ExpandString", "[registry]") +{ + std::wstring valueName = L"TestValueName"; + std::wstring valueValue = L"%TEMP%"; + + wil::unique_hkey root = RegCreateVolatileTestRoot(); + SetRegistryValue(root.get(), valueName, valueValue, REG_EXPAND_SZ); + + Key key{ root.get(), L"" }; + + auto value = key[valueName]; + REQUIRE(value); + REQUIRE(value->GetType() == Value::Type::ExpandString); + REQUIRE(value->GetValue<Value::Type::String>() == ConvertToUTF8(valueValue)); + + wchar_t buffer[MAX_PATH]; + GetTempPathW(ARRAYSIZE(buffer), buffer); + + std::string tempPath = ConvertToUTF8(buffer); + if (!tempPath.empty() && tempPath.back() == '\\') + { + tempPath.resize(tempPath.size() - 1); + } + + REQUIRE(value->GetValue<Value::Type::ExpandString>() == tempPath); +} + +TEST_CASE("Values_Binary", "[registry]") +{ + std::wstring valueName = L"TestValueName"; + std::vector<BYTE> valueValue = { 2, 7, 3, 14, 42 }; + + wil::unique_hkey root = RegCreateVolatileTestRoot(); + SetRegistryValue(root.get(), valueName, valueValue); + + Key key{ root.get(), L"" }; + + auto value = key[valueName]; + REQUIRE(value); + REQUIRE(value->GetType() == Value::Type::Binary); + + auto result = value->GetValue<Value::Type::Binary>(); + REQUIRE(result.size() == valueValue.size()); + for (size_t i = 0; i < result.size(); ++i) + { + INFO(i); + REQUIRE(result[i] == valueValue[i]); + } +} + +TEST_CASE("Values_DWORD", "[registry]") +{ + std::wstring valueName = L"TestValueName"; + DWORD valueValue = 42; + + wil::unique_hkey root = RegCreateVolatileTestRoot(); + SetRegistryValue(root.get(), valueName, valueValue); + + Key key{ root.get(), L"" }; + + auto value = key[valueName]; + REQUIRE(value); + REQUIRE(value->GetType() == Value::Type::DWord); + REQUIRE(value->GetValue<Value::Type::DWord>() == valueValue); +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -133,3 +133,17 @@ TEST_CASE("FoldCase", "[strings]") REQUIRE(FoldCase(u8"f\xF6ldcase"sv) == FoldCase(u8"F\xD6LDCASE"sv)); REQUIRE(FoldCase(u8"foldc\x430se"sv) == FoldCase(u8"FOLDC\x410SE"sv)); } + +TEST_CASE("ExpandEnvironmentVariables", "[strings]") +{ + wchar_t buffer[MAX_PATH]; + GetTempPathW(ARRAYSIZE(buffer), buffer); + + std::wstring tempPath = buffer; + if (!tempPath.empty() && tempPath.back() == '\\') + { + tempPath.resize(tempPath.size() - 1); + } + + REQUIRE(ExpandEnvironmentVariables(L"%TEMP%") == tempPath); +} diff --git a/src/AppInstallerCLITests/TestCommon.cpp b/src/AppInstallerCLITests/TestCommon.cpp @@ -32,6 +32,12 @@ namespace TestCommon static std::vector<std::filesystem::path> s_TempFilesOnFile; static std::filesystem::path s_TestDataFileBasePath{}; + + bool CleanVolatileTestRoot(HKEY root) + { + THROW_IF_WIN32_ERROR(RegDeleteTreeW(root, nullptr)); + return true; + } } TempFile::TempFile(const std::string& baseName, const std::string& baseExt, bool deleteFileOnConstruction) @@ -142,4 +148,45 @@ namespace TestCommon { return {}; } + + wil::unique_hkey RegCreateVolatileTestRoot() + { + // First create/open the real test root + wil::unique_hkey root; + THROW_IF_WIN32_ERROR(RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\WinGet\\TestRoot", 0, nullptr, REG_OPTION_VOLATILE, KEY_ALL_ACCESS, nullptr, &root, nullptr)); + + static bool s_ignored = CleanVolatileTestRoot(root.get()); + + // Create a random name + GUID name{}; + (void)CoCreateGuid(&name); + + wchar_t nameBuffer[256]; + (void)StringFromGUID2(name, nameBuffer, ARRAYSIZE(nameBuffer)); + + return RegCreateVolatileSubKey(root.get(), nameBuffer); + } + + wil::unique_hkey RegCreateVolatileSubKey(HKEY parent, const std::wstring& name) + { + wil::unique_hkey result; + THROW_IF_WIN32_ERROR(RegCreateKeyExW(parent, name.c_str(), 0, nullptr, REG_OPTION_VOLATILE, KEY_ALL_ACCESS, nullptr, &result, nullptr)); + return result; + } + + void SetRegistryValue(HKEY key, const std::wstring& name, const std::wstring& value, DWORD type) + { + THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, type, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>(sizeof(wchar_t) * (value.size() + 1)))); + } + + void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector<BYTE>& value) + { + THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, REG_BINARY, reinterpret_cast<const BYTE*>(value.data()), static_cast<DWORD>(value.size()))); + } + + void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value) + { + + THROW_IF_WIN32_ERROR(RegSetValueExW(key, name.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&value), sizeof(DWORD))); + } } diff --git a/src/AppInstallerCLITests/TestCommon.h b/src/AppInstallerCLITests/TestCommon.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerLogging.h> #include <AppInstallerProgress.h> #include <wil/result.h> @@ -79,7 +80,7 @@ namespace TestCommon std::string describe() const override { std::ostringstream result; - result << "has HR == 0x" << std::hex << std::setfill('0') << std::setw(8) << m_expectedHR; + result << "has HR == 0x" << AppInstaller::Logging::SetHRFormat << m_expectedHR; return result.str(); } @@ -97,4 +98,15 @@ namespace TestCommon std::function<void(uint64_t, uint64_t, AppInstaller::ProgressType)> m_OnProgress; }; + // Creates a volatile key for testing. + wil::unique_hkey RegCreateVolatileTestRoot(); + + // Creates a volatile subkey for testing. + wil::unique_hkey RegCreateVolatileSubKey(HKEY parent, const std::wstring& name); + + // Set registry values. + void SetRegistryValue(HKEY key, const std::wstring& name, const std::wstring& value, DWORD type = REG_SZ); + void SetRegistryValue(HKEY key, const std::wstring& name, const std::vector<BYTE>& value); + void SetRegistryValue(HKEY key, const std::wstring& name, DWORD value); + } diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -72,7 +72,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer1.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF8C82")); REQUIRE(installer1.Language == "en-US"); REQUIRE(installer1.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(installer1.Scope == "user"); + REQUIRE(installer1.Scope == ManifestInstaller::ScopeEnum::User); REQUIRE(installer1.PackageFamilyName == ""); REQUIRE(installer1.ProductCode == ""); REQUIRE(installer1.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::Install); @@ -93,7 +93,7 @@ TEST_CASE("ReadGoodManifestAndVerifyContents", "[ManifestValidation]") REQUIRE(installer2.Sha256 == SHA256::ConvertToBytes("69D84CA8899800A5575CE31798293CD4FEBAB1D734A07C2E51E56A28E0DF0000")); REQUIRE(installer2.Language == "en-US"); REQUIRE(installer2.InstallerType == ManifestInstaller::InstallerTypeEnum::Zip); - REQUIRE(installer2.Scope == "user"); + REQUIRE(installer2.Scope == ManifestInstaller::ScopeEnum::User); REQUIRE(installer2.PackageFamilyName == ""); REQUIRE(installer2.ProductCode == ""); REQUIRE(installer2.UpdateBehavior == ManifestInstaller::UpdateBehaviorEnum::UninstallPrevious); diff --git a/src/AppInstallerCLITests/pch.h b/src/AppInstallerCLITests/pch.h @@ -5,6 +5,7 @@ #include <Windows.h> #include <WinInet.h> #include <shellapi.h> +#include <objbase.h> #include <urlmon.h> #include <catch.hpp> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -270,6 +270,7 @@ <ClInclude Include="Public\winget\ManifestLocalization.h" /> <ClInclude Include="Public\winget\ManifestValidation.h" /> <ClInclude Include="Public\winget\ManifestYamlParser.h" /> + <ClInclude Include="Public\winget\Registry.h" /> <ClInclude Include="Public\winget\Settings.h" /> <ClInclude Include="Public\winget\UserSettings.h" /> <ClInclude Include="Public\winget\Yaml.h" /> @@ -309,6 +310,7 @@ <ClCompile Include="MsixInfo.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> </ClCompile> + <ClCompile Include="Registry.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -138,6 +138,9 @@ <ClInclude Include="Public\winget\ManifestYamlParser.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\Registry.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -230,6 +233,9 @@ <ClCompile Include="Manifest\YamlParser.cpp"> <Filter>Manifest</Filter> </ClCompile> + <ClCompile Include="Registry.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/AppInstallerLogging.cpp b/src/AppInstallerCommonCore/AppInstallerLogging.cpp @@ -138,6 +138,11 @@ namespace AppInstaller::Logging { FileLogger::BeginCleanup(Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation)); } + + std::ostream& SetHRFormat(std::ostream& out) + { + return out << std::hex << std::setw(8) << std::setfill('0'); + } } std::ostream& operator<<(std::ostream& out, const std::chrono::system_clock::time_point& time) diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -411,4 +411,27 @@ namespace AppInstaller::Utility return result; } + + std::wstring ExpandEnvironmentVariables(const std::wstring& input) + { + if (input.empty()) + { + return {}; + } + + DWORD charCount = ExpandEnvironmentStringsW(input.c_str(), nullptr, 0); + THROW_LAST_ERROR_IF(charCount == 0); + + std::wstring result(wil::safe_cast<size_t>(charCount), L'\0'); + + DWORD charCountWritten = ExpandEnvironmentStringsW(input.c_str(), &result[0], charCount); + THROW_HR_IF(E_UNEXPECTED, charCount != charCountWritten); + + if (result.back() == L'\0') + { + result.resize(result.size() - 1); + } + + return result; + } } diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -189,7 +189,7 @@ namespace AppInstaller::Logging TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); } - AICLI_LOG(CLI, Error, << "Terminating context: 0x" << std::hex << std::setw(8) << std::setfill('0') << hr << " at " << file << ":" << line); + AICLI_LOG(CLI, Error, << "Terminating context: 0x" << SetHRFormat << hr << " at " << file << ":" << line); } void TelemetryTraceLogger::LogException(std::string_view commandName, std::string_view type, std::string_view message) noexcept @@ -421,6 +421,27 @@ namespace AppInstaller::Logging } AICLI_LOG(CLI, Error, << type << " installer failed: " << errorCode); + } + + void TelemetryTraceLogger::LogDuplicateARPEntry(HRESULT hr, std::string_view scope, std::string_view architecture, std::string_view productCode, std::string_view name) + { + if (IsTelemetryEnabled()) + { + TraceLoggingWriteActivity(g_hTelemetryProvider, + "DuplicateARPEntry", + GetActivityId(), + nullptr, + TraceLoggingUInt32(s_subExecutionId, "SubExecutionId"), + TraceLoggingHResult(hr, "HResult"), + AICLI_TraceLoggingStringView(scope, "Scope"), + AICLI_TraceLoggingStringView(architecture, "Architecture"), + AICLI_TraceLoggingStringView(productCode, "ProductCode"), + AICLI_TraceLoggingStringView(name, "Name"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, << "Ignoring duplicate ARP entry " << scope << '|' << architecture << '|' << productCode << " [" << name << "]"); } void EnableWilFailureTelemetry() diff --git a/src/AppInstallerCommonCore/Architecture.cpp b/src/AppInstallerCommonCore/Architecture.cpp @@ -83,6 +83,25 @@ namespace AppInstaller::Utility return Architecture::Unknown; } + std::string_view ToString(Architecture architecture) + { + switch (architecture) + { + case Architecture::Neutral: + return "Neutral"sv; + case Architecture::X86: + return "X86"sv; + case Architecture::X64: + return "X64"sv; + case Architecture::Arm: + return "Arm"sv; + case Architecture::Arm64: + return "Arm64"sv; + } + + return "Unknown"sv; + } + Architecture GetSystemArchitecture() { Architecture systemArchitecture = Architecture::Unknown; diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -3,6 +3,7 @@ #pragma once #include "pch.h" #include "Public/AppInstallerErrors.h" +#include "Public/AppInstallerLogging.h" #include "Public/AppInstallerStrings.h" @@ -109,7 +110,7 @@ namespace AppInstaller void GetUserPresentableMessageForHR(std::ostringstream& strstr, HRESULT hr) { - strstr << "0x" << std::hex << std::setw(8) << std::setfill('0') << hr << " : "; + strstr << "0x" << Logging::SetHRFormat << hr << " : "; if (HRESULT_FACILITY(hr) == APPINSTALLER_CLI_ERROR_FACILITY) { diff --git a/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp b/src/AppInstallerCommonCore/Manifest/ManifestInstaller.cpp @@ -97,46 +97,62 @@ namespace AppInstaller::Manifest return result; } - std::string ManifestInstaller::InstallerTypeToString(ManifestInstaller::InstallerTypeEnum installerType) + ManifestInstaller::ScopeEnum ManifestInstaller::ConvertToScopeEnum(const std::string& in) { - std::string result = "Unknown"; + ScopeEnum result = ScopeEnum::Unknown; - switch (installerType) + if (Utility::CaseInsensitiveEquals(in, "user")) + { + result = ScopeEnum::User; + } + else if (Utility::CaseInsensitiveEquals(in, "machine")) { - case ManifestInstaller::InstallerTypeEnum::Exe: - result = "Exe"; - break; - case ManifestInstaller::InstallerTypeEnum::Inno: - result = "Inno"; - break; - case ManifestInstaller::InstallerTypeEnum::Msi: - result = "Msi"; - break; - case ManifestInstaller::InstallerTypeEnum::Msix: - result = "Msix"; - break; - case ManifestInstaller::InstallerTypeEnum::Nullsoft: - result = "Nullsoft"; - break; - case ManifestInstaller::InstallerTypeEnum::Wix: - result = "Wix"; - break; - case ManifestInstaller::InstallerTypeEnum::Zip: - result = "Zip"; - break; - case ManifestInstaller::InstallerTypeEnum::Burn: - result = "Burn"; - break; - case ManifestInstaller::InstallerTypeEnum::MSStore: - result = "MSStore"; - break; - default: - break; + result = ScopeEnum::Machine; } return result; } + std::string_view ManifestInstaller::InstallerTypeToString(ManifestInstaller::InstallerTypeEnum installerType) + { + switch (installerType) + { + case InstallerTypeEnum::Exe: + return "Exe"sv; + case InstallerTypeEnum::Inno: + return "Inno"sv; + case InstallerTypeEnum::Msi: + return "Msi"sv; + case InstallerTypeEnum::Msix: + return "Msix"sv; + case InstallerTypeEnum::Nullsoft: + return "Nullsoft"sv; + case InstallerTypeEnum::Wix: + return "Wix"sv; + case InstallerTypeEnum::Zip: + return "Zip"sv; + case InstallerTypeEnum::Burn: + return "Burn"sv; + case InstallerTypeEnum::MSStore: + return "MSStore"sv; + } + + return "Unknown"sv; + } + + std::string_view ManifestInstaller::ScopeToString(ScopeEnum scope) + { + switch (scope) + { + case ScopeEnum::User: + return "User"sv; + case ScopeEnum::Machine: + return "Machine"sv; + } + + return "Unknown"sv; + } + bool ManifestInstaller::DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType) { return (installerType == InstallerTypeEnum::Msix || installerType == InstallerTypeEnum::MSStore); diff --git a/src/AppInstallerCommonCore/Manifest/YamlParser.cpp b/src/AppInstallerCommonCore/Manifest/YamlParser.cpp @@ -90,7 +90,7 @@ namespace AppInstaller::Manifest { "Sha256", [this](const YAML::Node& value) { m_p_installer->Sha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, { "SignatureSha256", [this](const YAML::Node& value) { m_p_installer->SignatureSha256 = Utility::SHA256::ConvertToBytes(value.as<std::string>()); }, false, "^[A-Fa-f0-9]{64}$" }, { "Language", [this](const YAML::Node& value) { m_p_installer->Language = value.as<std::string>(); } }, - { "Scope", [this](const YAML::Node& value) { m_p_installer->Scope = value.as<std::string>(); } }, + { "Scope", [this](const YAML::Node& value) { m_p_installer->Scope = ManifestInstaller::ConvertToScopeEnum(value.as<std::string>()); } }, { "InstallerType", [this](const YAML::Node& value) { m_p_installer->InstallerType = ManifestInstaller::ConvertToInstallerTypeEnum(value.as<std::string>()); } }, { "UpdateBehavior", [this](const YAML::Node& value) { m_p_installer->UpdateBehavior = ManifestInstaller::ConvertToUpdateBehaviorEnum(value.as<std::string>()); } }, { "PackageFamilyName", [this](const YAML::Node& value) { m_p_installer->PackageFamilyName = value.as<std::string>(); }, false, "[-.A-Za-z0-9]+_[A-Za-z0-9]{13}" }, @@ -250,7 +250,7 @@ namespace AppInstaller::Manifest // Populate defaults installer.InstallerType = manifest.InstallerType; installer.UpdateBehavior = manifest.UpdateBehavior; - installer.Scope = "user"; + installer.Scope = ManifestInstaller::ScopeEnum::User; m_p_installer = &installer; m_p_switchesNode = &installerSwitchesNode; diff --git a/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h b/src/AppInstallerCommonCore/Public/AppInstallerArchitecture.h @@ -21,6 +21,9 @@ namespace AppInstaller::Utility // Converts a string to corresponding enum Architecture ConvertToArchitectureEnum(const std::string& archStr); + // Converts an Architecture to a string_view + std::string_view ToString(Architecture architecture); + // Gets the system's architecture as Architecture enum AppInstaller::Utility::Architecture GetSystemArchitecture(); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerLogging.h b/src/AppInstallerCommonCore/Public/AppInstallerLogging.h @@ -137,6 +137,9 @@ namespace AppInstaller::Logging // Starts a background task to clean up old log files. void BeginLogFileCleanup(); + + // Calls the various stream format functions to produce an 8 character hexidecimal output. + std::ostream& SetHRFormat(std::ostream& out); } // Enable output of system_clock timepoints. diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -124,4 +124,7 @@ namespace AppInstaller::Utility // Reads the entire stream into a string. std::string ReadEntireStream(std::istream& stream); + + // Expands environment variables within the input. + std::wstring ExpandEnvironmentVariables(const std::wstring& input); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h @@ -89,6 +89,10 @@ namespace AppInstaller::Logging // Logs a faild installation attempt. void LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode); + // Logs a failure to insert a value into the in-memory cache of installed system packages. + // The most likely reason is due to the same key name being used under multiple ARP scope/architecture locations. + void LogDuplicateARPEntry(HRESULT hr, std::string_view scope, std::string_view architecture, std::string_view productCode, std::string_view name); + private: TelemetryTraceLogger(); }; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h b/src/AppInstallerCommonCore/Public/winget/ManifestInstaller.h @@ -52,6 +52,13 @@ namespace AppInstaller::Manifest Update }; + enum class ScopeEnum + { + Unknown, + User, + Machine, + }; + // Required. Values: x86, x64, arm, arm64, all. AppInstaller::Utility::Architecture Arch; @@ -69,7 +76,7 @@ namespace AppInstaller::Manifest string_t Language; // Name TBD - string_t Scope; + ScopeEnum Scope; // Store Product Id string_t ProductId; @@ -93,7 +100,11 @@ namespace AppInstaller::Manifest static UpdateBehaviorEnum ConvertToUpdateBehaviorEnum(const std::string& in); - static std::string InstallerTypeToString(InstallerTypeEnum installerType); + static ScopeEnum ConvertToScopeEnum(const std::string& in); + + static std::string_view InstallerTypeToString(InstallerTypeEnum installerType); + + static std::string_view ScopeToString(ScopeEnum scope); // Gets a value indicating whether the given installer type uses the PackageFamilyName system reference. static bool DoesInstallerTypeUsePackageFamilyName(InstallerTypeEnum installerType); diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -58,6 +58,9 @@ namespace AppInstaller::Manifest ValidationError(std::string message, std::string field) : Message(std::move(message)), Field(std::move(field)) {} + ValidationError(std::string message, std::string field, std::string_view value) : + Message(std::move(message)), Field(std::move(field)), Value(value) {} + ValidationError(std::string message, std::string field, std::string value) : Message(std::move(message)), Field(std::move(field)), Value(std::move(value)) {} diff --git a/src/AppInstallerCommonCore/Public/winget/Registry.h b/src/AppInstallerCommonCore/Public/winget/Registry.h @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <wil/resource.h> + +#include <optional> +#include <string> +#include <string_view> +#include <vector> + + +namespace AppInstaller::Registry +{ + namespace details + { + template <DWORD Type> + struct ValueTypeSpecifics + { + using value_t = void; + + static value_t Convert(const std::vector<BYTE>& data) + { + static_assert(false, "No Type specific override has been supplied"); + } + }; + + template <> + struct ValueTypeSpecifics<REG_NONE> + { + using value_t = std::vector<BYTE>; + static value_t Convert(const std::vector<BYTE>& data); + }; + + template <> + struct ValueTypeSpecifics<REG_SZ> + { + using value_t = std::string; + static value_t Convert(const std::vector<BYTE>& data); + }; + + template <> + struct ValueTypeSpecifics<REG_EXPAND_SZ> + { + using value_t = std::string; + static value_t Convert(const std::vector<BYTE>& data); + }; + + template <> + struct ValueTypeSpecifics<REG_BINARY> + { + using value_t = std::vector<BYTE>; + static value_t Convert(const std::vector<BYTE>& data); + }; + + template <> + struct ValueTypeSpecifics<REG_DWORD_LITTLE_ENDIAN> + { + using value_t = uint32_t; + static value_t Convert(const std::vector<BYTE>& data); + }; + } + + struct Key; + + // A registry value. + struct Value + { + friend Key; + + // The type of data stored in the Value. + enum class Type : DWORD + { + None = REG_NONE, + String = REG_SZ, + ExpandString = REG_EXPAND_SZ, + Binary = REG_BINARY, + DWord = REG_DWORD, + DWordLittleEndian = REG_DWORD_LITTLE_ENDIAN, + DWordBigEndian = REG_DWORD_BIG_ENDIAN, + MultiString = REG_MULTI_SZ, + QWord = REG_QWORD, + QWordLittleEndian = REG_QWORD_LITTLE_ENDIAN, + }; + + Type GetType() const { return m_type; } + + template <Type T> + typename details::ValueTypeSpecifics<static_cast<DWORD>(T)>::value_t GetValue() const + { + EnsureType(T); + return details::ValueTypeSpecifics<static_cast<DWORD>(T)>::Convert(m_data); + } + + private: + Value(DWORD type, std::vector<BYTE>&& data); + + void EnsureType(Type type) const; + + Type m_type; + std::vector<BYTE> m_data; + }; + + // A registry key. + struct Key + { + Key() = default; + Key(HKEY key); + Key(HKEY key, std::string_view subKey, DWORD options = 0, REGSAM access = KEY_READ); + Key(HKEY key, const std::wstring& subKey, DWORD options = 0, REGSAM access = KEY_READ); + + // --== Sub-Key iteration ==-- + struct const_iterator; + + struct SubKeyRef + { + friend const_iterator; + + // Gets the name of the subkey. + std::string Name() const; + + // Opens the subkey. + Key Open() const; + + operator bool() const { return m_parentKey.operator bool(); } + + private: + // For a valid iterator + SubKeyRef(const wil::shared_hkey& key, REGSAM access); + + // For the end iterator + SubKeyRef() = default; + + // Enumerates the subkey of m_parentKey at the given index. + void Enum(DWORD index); + + wil::shared_hkey m_parentKey; + REGSAM m_access = KEY_READ; + std::wstring m_subKeyName; + }; + + struct const_iterator + { + friend Key; + + const_iterator& operator++(); + const_iterator operator++(int); + + bool operator==(const const_iterator& other) const; + bool operator!=(const const_iterator& other) const; + + const SubKeyRef& operator*() const; + const SubKeyRef* operator->() const; + + private: + // Create an iterator for begin + const_iterator(const wil::shared_hkey& key, REGSAM access); + + // Create an iterator for end + const_iterator() = default; + + DWORD m_index = 0; + SubKeyRef m_subkey; + }; + + const_iterator begin() const; + const_iterator end() const; + + std::optional<Value> operator[](std::string_view name) const; + std::optional<Value> operator[](const std::wstring& name) const; + + operator bool() const { return m_key.operator bool(); } + + // Open a Key; will return an empty Key if the subkey does not exist. + static Key OpenIfExists(HKEY key, std::string_view subKey = {}, DWORD options = 0, REGSAM access = KEY_READ); + static Key OpenIfExists(HKEY key, const std::wstring& subKey = {}, DWORD options = 0, REGSAM access = KEY_READ); + + private: + void Initialize(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access, bool ignoreErrorIfDoesNotExist); + + wil::shared_hkey m_key; + REGSAM m_access = KEY_READ; + }; +} diff --git a/src/AppInstallerCommonCore/Registry.cpp b/src/AppInstallerCommonCore/Registry.cpp @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/winget/Registry.h" +#include "Public/AppInstallerStrings.h" +#include "Public/AppInstallerLogging.h" + + +namespace AppInstaller::Registry +{ + namespace + { + std::wstring_view ConvertBytesToWideStringView(const std::vector<BYTE>& data) + { + THROW_HR_IF(E_NOT_VALID_STATE, (data.size() % sizeof(wchar_t)) != 0); + std::wstring_view result{ reinterpret_cast<const wchar_t*>(data.data()), data.size() / sizeof(wchar_t) }; + + // Registry values may or may not be null terminated; we will remove any trailing nulls + while (!result.empty() && result.back() == L'\0') + { + result = result.substr(0, result.size() - 1); + } + + return result; + } + + std::wstring ConvertBytesToWideString(const std::vector<BYTE>& data) + { + return std::wstring{ ConvertBytesToWideStringView(data) }; + } + + std::string ConvertBytesToString(const std::vector<BYTE>& data) + { + return Utility::ConvertToUTF8(ConvertBytesToWideStringView(data)); + } + + uint32_t ConvertBytesToUInt32LE(const std::vector<BYTE>& data) + { + THROW_HR_IF(E_NOT_VALID_STATE, data.size() != sizeof(uint32_t)); + uint32_t result = 0; + uint32_t shift = 0; + + for (const BYTE datum : data) + { + result |= ((static_cast<uint32_t>(datum) & 0xFF) << shift); + shift += 8; + } + + return result; + } + } + + namespace details + { + ValueTypeSpecifics<REG_NONE>::value_t ValueTypeSpecifics<REG_NONE>::Convert(const std::vector<BYTE>& data) + { + return data; + } + + ValueTypeSpecifics<REG_SZ>::value_t ValueTypeSpecifics<REG_SZ>::Convert(const std::vector<BYTE>& data) + { + return ConvertBytesToString(data); + } + + ValueTypeSpecifics<REG_EXPAND_SZ>::value_t ValueTypeSpecifics<REG_EXPAND_SZ>::Convert(const std::vector<BYTE>& data) + { + return Utility::ConvertToUTF8(Utility::ExpandEnvironmentVariables(ConvertBytesToWideString(data))); + } + + ValueTypeSpecifics<REG_BINARY>::value_t ValueTypeSpecifics<REG_BINARY>::Convert(const std::vector<BYTE>& data) + { + return data; + } + + ValueTypeSpecifics<REG_DWORD_LITTLE_ENDIAN>::value_t ValueTypeSpecifics<REG_DWORD_LITTLE_ENDIAN>::Convert(const std::vector<BYTE>& data) + { + return ConvertBytesToUInt32LE(data); + } + } + + Value::Value(DWORD type, std::vector<BYTE>&& data) : m_type(static_cast<Type>(type)), m_data(std::move(data)) + { + } + + void Value::EnsureType(Type type) const + { + // Allow interop between String and ExpandString + if ((m_type == Type::String || m_type == Type::ExpandString) && (type == Type::String || type == Type::ExpandString)) + { + return; + } + + THROW_HR_IF(E_INVALIDARG, m_type != type); + } + + Key::Key(HKEY key) + { + Initialize(key, {}, 0, KEY_READ, false); + } + + Key::Key(HKEY key, std::string_view subKey, DWORD options, REGSAM access) + { + Initialize(key, Utility::ConvertToUTF16(subKey), options, access, false); + } + + Key::Key(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access) + { + Initialize(key, subKey, options, access, false); + } + + std::string Key::SubKeyRef::Name() const + { + return Utility::ConvertToUTF8(m_subKeyName); + } + + Key Key::SubKeyRef::Open() const + { + return { m_parentKey.get(), m_subKeyName, 0, m_access }; + } + + Key::SubKeyRef::SubKeyRef(const wil::shared_hkey& key, REGSAM access) : + m_parentKey(key), m_access(access), m_subKeyName(64, L'\0') + { + Enum(0); + } + + void Key::SubKeyRef::Enum(DWORD index) + { + LSTATUS status = ERROR_SUCCESS; + DWORD charCount = 0; + + while (m_subKeyName.size() < 4096) + { + charCount = wil::safe_cast<DWORD>(m_subKeyName.size()); + status = RegEnumKeyExW(m_parentKey.get(), index, &m_subKeyName[0], &charCount, nullptr, nullptr, nullptr, nullptr); + + if (status == ERROR_MORE_DATA) + { + // See if we can get away with the current capacity + if (m_subKeyName.size() < m_subKeyName.capacity()) + { + m_subKeyName.resize(m_subKeyName.capacity()); + } + else + { + m_subKeyName.resize(m_subKeyName.capacity() * 2); + } + } + else + { + break; + } + } + + if (status == ERROR_SUCCESS) + { + m_subKeyName.resize(wil::safe_cast<size_t>(charCount)); + } + else if (status == ERROR_NO_MORE_ITEMS) + { + m_parentKey.reset(); + } + else + { + THROW_IF_WIN32_ERROR(status); + } + } + + Key::const_iterator& Key::const_iterator::operator++() + { + m_subkey.Enum(++m_index); + return *this; + } + + Key::const_iterator Key::const_iterator::operator++(int) + { + const_iterator result = *this; + m_subkey.Enum(++m_index); + return result; + } + + bool Key::const_iterator::operator==(const const_iterator& other) const + { + return (!m_subkey.m_parentKey && !other.m_subkey.m_parentKey) || (m_subkey.m_parentKey.get() == other.m_subkey.m_parentKey.get() && m_index == other.m_index); + } + + bool Key::const_iterator::operator!=(const const_iterator& other) const + { + return !operator==(other); + } + + const Key::SubKeyRef& Key::const_iterator::operator*() const + { + return m_subkey; + } + + const Key::SubKeyRef* Key::const_iterator::operator->() const + { + return &m_subkey; + } + + Key::const_iterator::const_iterator(const wil::shared_hkey& key, REGSAM access) : + m_subkey(key, access) + { + } + + Key::const_iterator Key::begin() const + { + return { m_key, m_access }; + } + + Key::const_iterator Key::end() const + { + return {}; + } + + std::optional<Value> Key::operator[](std::string_view name) const + { + return operator[](Utility::ConvertToUTF16(name)); + } + + std::optional<Value> Key::operator[](const std::wstring& name) const + { + std::vector<BYTE> data; + data.resize(64); + + LSTATUS status = ERROR_SUCCESS; + DWORD type = 0; + DWORD byteCount = 0; + + while (data.size() < (64 << 20)) + { + byteCount = wil::safe_cast<DWORD>(data.size()); + status = RegQueryValueExW(m_key.get(), name.c_str(), nullptr, &type, data.data(), &byteCount); + + if (status == ERROR_MORE_DATA && byteCount > data.size()) + { + data.resize(byteCount); + } + else + { + break; + } + } + + if (status == ERROR_FILE_NOT_FOUND) + { + return {}; + } + + THROW_IF_WIN32_ERROR(status); + + // Resize to actual data size + data.resize(byteCount); + + return Value{ type, std::move(data) }; + } + + Key Key::OpenIfExists(HKEY key, std::string_view subKey, DWORD options, REGSAM access) + { + return OpenIfExists(key, Utility::ConvertToUTF16(subKey), options, access); + } + + Key Key::OpenIfExists(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access) + { + Key result; + result.Initialize(key, subKey, options, access, true); + return result; + } + + void Key::Initialize(HKEY key, const std::wstring& subKey, DWORD options, REGSAM access, bool ignoreErrorIfDoesNotExist) + { + LSTATUS status = RegOpenKeyExW(key, subKey.c_str(), options, access, &m_key); + + if (ignoreErrorIfDoesNotExist && status == ERROR_FILE_NOT_FOUND) + { + AICLI_LOG(Core, Verbose, << "Subkey '" << Utility::ConvertToUTF8(subKey) << "' was not found"); + return; + } + + THROW_IF_WIN32_ERROR(status); + } +} diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -146,6 +146,7 @@ namespace AppInstaller::Utility Version Version::CreateLatest() { Version result; + result.m_version = s_Version_Part_Latest; result.m_parts.emplace_back(0, std::string{ s_Version_Part_Latest }); return result; } @@ -158,6 +159,7 @@ namespace AppInstaller::Utility Version Version::CreateUnknown() { Version result; + result.m_version = s_Version_Part_Unknown; result.m_parts.emplace_back(0, std::string{ s_Version_Part_Unknown }); return result; } diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -37,9 +37,9 @@ #include <type_traits> #include <vector> +#include <wil/resource.h> #include <wil/result_macros.h> #include <wil/safecast.h> -#include <wil/resource.h> #include <wil/token_helpers.h> #ifndef WINGET_DISABLE_FOR_FUZZING diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -174,6 +174,7 @@ <ItemGroup> <ClInclude Include="CompositeSource.h" /> <ClInclude Include="ICU\SQLiteICU.h" /> + <ClInclude Include="Microsoft\ARPHelper.h" /> <ClInclude Include="Microsoft\PredefinedInstalledSourceFactory.h" /> <ClInclude Include="Microsoft\PreIndexedPackageSourceFactory.h" /> <ClInclude Include="Microsoft\Schema\1_0\ChannelTable.h" /> @@ -219,6 +220,7 @@ <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> </ClCompile> + <ClCompile Include="Microsoft\ARPHelper.cpp" /> <ClCompile Include="Microsoft\PredefinedInstalledSourceFactory.cpp" /> <ClCompile Include="Microsoft\PreIndexedPackageSourceFactory.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\Interface_1_0.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -135,6 +135,9 @@ <ClInclude Include="Microsoft\Schema\1_1\ManifestMetadataTable.h"> <Filter>Microsoft\Schema\1_1</Filter> </ClInclude> + <ClInclude Include="Microsoft\ARPHelper.h"> + <Filter>Microsoft</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -203,6 +206,9 @@ <ClCompile Include="Microsoft\Schema\1_1\ManifestMetadataTable.cpp"> <Filter>Microsoft\Schema\1_1</Filter> </ClCompile> + <ClCompile Include="Microsoft\ARPHelper.cpp"> + <Filter>Microsoft</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/CompositeSource.cpp b/src/AppInstallerRepositoryCore/CompositeSource.cpp @@ -220,9 +220,9 @@ namespace AppInstaller::Repository bool operator<(const SystemReferenceString& other) const { - if (Field < other.Field) + if (Field != other.Field) { - return true; + return Field < other.Field; } return String < other.String; diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ARPHelper.h" + +namespace AppInstaller::Repository::Microsoft +{ + Registry::Key ARPHelper::GetARPKey(Manifest::ManifestInstaller::ScopeEnum scope, Utility::Architecture architecture) const + { + HKEY rootKey = NULL; + + switch (scope) + { + case Manifest::ManifestInstaller::ScopeEnum::User: + rootKey = HKEY_CURRENT_USER; + break; + case Manifest::ManifestInstaller::ScopeEnum::Machine: + rootKey = HKEY_LOCAL_MACHINE; + break; + default: + THROW_HR(E_UNEXPECTED); + } + + bool isValid = false; + REGSAM access = KEY_READ; + + switch (Utility::GetSystemArchitecture()) + { + case Utility::Architecture::X86: + switch (architecture) + { + case Utility::Architecture::X86: + isValid = true; + break; + } + break; + case Utility::Architecture::X64: + switch (architecture) + { + case Utility::Architecture::X86: + if (scope == Manifest::ManifestInstaller::ScopeEnum::Machine) + { + access |= KEY_WOW64_32KEY; + isValid = true; + } + break; + case Utility::Architecture::X64: + access |= KEY_WOW64_64KEY; + isValid = true; + break; + } + break; + case Utility::Architecture::Arm: + switch (architecture) + { + case Utility::Architecture::Arm: + isValid = true; + break; + } + break; + case Utility::Architecture::Arm64: + switch (architecture) + { + case Utility::Architecture::X86: + if (scope == Manifest::ManifestInstaller::ScopeEnum::Machine) + { +#ifdef _ARM_ + // Not accessible if this is an ARM process + AICLI_LOG(Repo, Warning, << "Cannot enumerate x86 machine ARP entries when current process is ARM"); +#else + access |= KEY_WOW64_32KEY; + isValid = true; +#endif + } + break; + case Utility::Architecture::Arm64: + access |= KEY_WOW64_64KEY; + isValid = true; + break; + } + break; + } + + if (isValid) + { + return Registry::Key::OpenIfExists(rootKey, SubKeyPath, 0, access); + } + else + { + return {}; + } + } + + bool ARPHelper::GetBoolValue(const Registry::Key& arpKey, const std::wstring& name) + { + auto value = arpKey[name]; + return (value && value->GetType() == Registry::Value::Type::DWord && value->GetValue<Registry::Value::Type::DWord>()); + } + + std::string ARPHelper::DetermineVersion(const Registry::Key& arpKey) const + { + auto displayVersion = arpKey[DisplayVersion]; + if (displayVersion && displayVersion->GetType() == Registry::Value::Type::String) + { + std::string result = displayVersion->GetValue<Registry::Value::Type::String>(); + if (!result.empty()) + { + return result; + } + } + + auto version = arpKey[Version]; + if (version && version->GetType() == Registry::Value::Type::DWord) + { + uint32_t versionInt = version->GetValue<Registry::Value::Type::DWord>(); + if (versionInt) + { + std::ostringstream strstr; + strstr << ((versionInt & 0xFF000000) >> 24) << '.' << ((versionInt & 0x00FF0000) >> 16) << '.' << (versionInt & 0x0000FFFF); + return strstr.str(); + } + } + + auto majorVersion = arpKey[VersionMajor]; + auto minorVersion = arpKey[VersionMinor]; + if (majorVersion || minorVersion) + { + uint32_t majorVersionInt = 0; + uint32_t minorVersionInt = 0; + + if (majorVersion && majorVersion->GetType() == Registry::Value::Type::DWord) + { + majorVersionInt = majorVersion->GetValue<Registry::Value::Type::DWord>(); + } + + if (minorVersion && minorVersion->GetType() == Registry::Value::Type::DWord) + { + minorVersionInt = minorVersion->GetValue<Registry::Value::Type::DWord>(); + } + + if (majorVersionInt || minorVersionInt) + { + std::ostringstream strstr; + strstr << majorVersionInt << '.' << minorVersionInt; + return strstr.str(); + } + } + + return Utility::Version::CreateUnknown().ToString(); + } + + void ARPHelper::AddMetadataIfPresent(const Registry::Key& key, const std::wstring& name, SQLiteIndex& index, SQLiteIndex::IdType manifestId, PackageVersionMetadata metadata) + { + auto value = key[name]; + if (value && value->GetType() == Registry::Value::Type::String) + { + auto valueString = value->GetValue<Registry::Value::Type::String>(); + if (!valueString.empty()) + { + index.SetMetadataByManifestId(manifestId, metadata, valueString); + } + } + } + + void ARPHelper::PopulateIndexFromARP(SQLiteIndex& index, Manifest::ManifestInstaller::ScopeEnum scope) const + { + for (auto architecture : Utility::GetApplicableArchitectures()) + { + Registry::Key arpRootKey = GetARPKey(scope, architecture); + + if (arpRootKey) + { + PopulateIndexFromKey(index, arpRootKey, Manifest::ManifestInstaller::ScopeToString(scope), Utility::ToString(architecture)); + } + } + } + + void ARPHelper::PopulateIndexFromKey(SQLiteIndex& index, const Registry::Key& key, std::string_view scope, std::string_view architecture) const + { + AICLI_LOG(Repo, Info, << "Examining ARP entries for " << scope << " | " << architecture); + + for (const auto& arpEntry : key) + { + std::string productCode = arpEntry.Name(); + + Manifest::Manifest manifest; + manifest.Tags = { "ARP" }; + + // Use the key name as the Id, as it is supposed to be unique. + // TODO: We probably want something better here, like constructing the value as + // `Publisher.DisplayName`. We would need to ensure that there are no matches + // against the rest of the data however (might happen if same package is + // installed for multiple architectures/languages). + manifest.Id = productCode; + + manifest.Installers.emplace_back(); + // TODO: This likely needs some cleanup applied, as it looks like INNO tends to append an "_is#" + // that might vary across machines/installs. There may be other things we want to clean up as well, + // like trimming spaces at the ends, or removing the version string from the product code + // if it is present. + manifest.Installers[0].ProductCode = productCode; + + Registry::Key arpKey = arpEntry.Open(); + + // Ignore entries that are listed as SystemComponent + if (GetBoolValue(arpKey, SystemComponent)) + { + AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because it is a SystemComponent"); + continue; + } + + // If no name is provided, ignore this entry + auto displayName = arpKey[DisplayName]; + if (!displayName || displayName->GetType() != Registry::Value::Type::String) + { + AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is not a REG_SZ value"); + continue; + } + manifest.Name = displayName->GetValue<Registry::Value::Type::String>(); + if (manifest.Name.empty()) + { + AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because DisplayName is empty"); + continue; + } + + // If no version can be determined, ignore this entry + manifest.Version = DetermineVersion(arpKey); + if (manifest.Version.empty()) + { + AICLI_LOG(Repo, Verbose, << "Skipping " << productCode << " because a version could not be determined"); + continue; + } + + auto publisher = arpKey[Publisher]; + if (publisher && publisher->GetType() == Registry::Value::Type::String) + { + manifest.Publisher = publisher->GetValue<Registry::Value::Type::String>(); + } + + // TODO: If we want to keep the constructed manifest around to allow for `show` type commands + // against installed packages, we should use URLInfoAbout/HelpLink for the Homepage. + + // TODO: Pick up Language/InnoSetupLanguage to enable proper selection of language for upgrade. + + // TODO: Determine the best way to handle duplicates, which may very well happen. + // For now, we will attempt to insert and catch, then send failure telemetry. + // In a future where we cache these entries + std::optional<SQLiteIndex::IdType> manifestIdOpt; + HRESULT addHr = S_OK; + + try + { + // Use the ProductCode as a unique key for the path + manifestIdOpt = index.AddManifest(manifest, Utility::ConvertToUTF16(manifest.Installers[0].ProductCode)); + } + catch (wil::ResultException& re) + { + addHr = re.GetErrorCode(); + } + catch (...) + { + addHr = E_FAIL; + } + + if (!manifestIdOpt) + { + Logging::Telemetry().LogDuplicateARPEntry(addHr, scope, architecture, productCode, manifest.Name); + continue; + } + + SQLiteIndex::IdType manifestId = manifestIdOpt.value(); + + // Pass scope along to metadata. + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledScope, scope); + + // TODO: Pass along architecture, although there are cases where it is not clear what architecture the package + // is from it's ARP location, despite it very clearly being a specific architecture. And note that user + // scope does not have separate ARP locations, so every architecture would appear as native. + + // Pick up InstallLocation when upgrade supports remove/install to enable this location + // to survive across the removal. + AddMetadataIfPresent(arpKey, InstallLocation, index, manifestId, PackageVersionMetadata::InstalledLocation); + + // Pick up UninstallString and QuietUninstallString for uninstall. + AddMetadataIfPresent(arpKey, UninstallString, index, manifestId, PackageVersionMetadata::StandardUninstallCommand); + AddMetadataIfPresent(arpKey, QuietUninstallString, index, manifestId, PackageVersionMetadata::SilentUninstallCommand); + + // Pick up WindowsInstaller to determine if this is an MSI install. + // TODO: Could also determine Inno (and maybe other types) through detecting other keys here. + auto installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Exe; + + if (GetBoolValue(arpKey, WindowsInstaller)) + { + installedType = Manifest::ManifestInstaller::InstallerTypeEnum::Msi; + } + + index.SetMetadataByManifestId(manifestId, PackageVersionMetadata::InstalledType, Manifest::ManifestInstaller::InstallerTypeToString(installedType)); + } + } +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h b/src/AppInstallerRepositoryCore/Microsoft/ARPHelper.h @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/SQLiteIndex.h" +#include <AppInstallerArchitecture.h> +#include <winget/Registry.h> +#include <winget/ManifestInstaller.h> +#include <wil/resource.h> + +#include <string> + +namespace AppInstaller::Repository::Microsoft +{ + // A helper to find the various locations that contain ARP (Add/Remove Programs) entries. + struct ARPHelper + { + // See https://docs.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key for details. + std::wstring SubKeyPath{ L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall" }; + + // REG_SZ + std::wstring DisplayName{ L"DisplayName" }; + // REG_SZ + std::wstring Publisher{ L"Publisher" }; + // REG_SZ + std::wstring DisplayVersion{ L"DisplayVersion" }; + // REG_DWORD (ex. 0xMMmmbbbb, M[ajor], m[inor], b[uild]) + std::wstring Version{ L"Version" }; + // REG_DWORD + std::wstring VersionMajor{ L"VersionMajor" }; + // REG_DWORD + std::wstring VersionMinor{ L"VersionMinor" }; + // REG_SZ + std::wstring URLInfoAbout{ L"URLInfoAbout" }; + // REG_SZ + std::wstring HelpLink{ L"HelpLink" }; + // REG_SZ + std::wstring InstallLocation{ L"InstallLocation" }; + // REG_DWORD (ex. 1033 [en-us]) + std::wstring Language{ L"Language" }; + // REG_SZ (ex. "english") + std::wstring InnoSetupLanguage{ L"Inno Setup: Language" }; + // REG_EXPAND_SZ + std::wstring UninstallString{ L"UninstallString" }; + // REG_EXPAND_SZ + std::wstring QuietUninstallString{ L"QuietUninstallString" }; + // REG_DWORD (bool, true indicates MSI) + std::wstring WindowsInstaller{ L"WindowsInstaller" }; + // REG_DWORD (bool) + std::wstring SystemComponent{ L"SystemComponent" }; + + // Gets the registry key associated with the given scope and architecture on this platform. + // May return an empty key if there is no valid location (bad combination or not found). + Registry::Key GetARPKey(Manifest::ManifestInstaller::ScopeEnum scope, Utility::Architecture architecture) const; + + // Returns true IFF the value exists and contains a non-zero DWORD. + static bool GetBoolValue(const Registry::Key& arpKey, const std::wstring& name); + + // Determines the version from an ARP entry. + // The priority is: + // DisplayVersion + // Version + // MajorVerison, MinorVersion + std::string DetermineVersion(const Registry::Key& arpKey) const; + + // Reads a value and adds it to the metadata if it exists. + static void AddMetadataIfPresent(const Registry::Key& key, const std::wstring& name, SQLiteIndex& index, SQLiteIndex::IdType manifestId, PackageVersionMetadata metadata); + + // Populates the index with the ARP entries from the given scope (machine/user). + // Handles all of the architectures for the given scope. + void PopulateIndexFromARP(SQLiteIndex& index, Manifest::ManifestInstaller::ScopeEnum scope) const; + + // Populates the index with the ARP entries from the given key. + // This entry point is primarily to allow unit tests to operate of arbitrary keys; + // product code should use PopulateIndexFromARP. + void PopulateIndexFromKey(SQLiteIndex& index, const Registry::Key& key, std::string_view scope, std::string_view architecture) const; + }; +} diff --git a/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp b/src/AppInstallerRepositoryCore/Microsoft/PredefinedInstalledSourceFactory.cpp @@ -2,11 +2,15 @@ // Licensed under the MIT License. #pragma once #include "pch.h" +#include "Microsoft/ARPHelper.h" #include "Microsoft/PredefinedInstalledSourceFactory.h" #include "Microsoft/SQLiteIndex.h" #include "Microsoft/SQLiteIndexSource.h" #include <winget/ManifestInstaller.h> +#include <winget/Registry.h> +#include <AppInstallerArchitecture.h> + using namespace std::string_literals; using namespace std::string_view_literals; @@ -14,13 +18,6 @@ namespace AppInstaller::Repository::Microsoft { namespace { - // Populates the index with the ARP entries from the given root. - void PopulateIndexFromARP(SQLiteIndex& index, HKEY rootKey) - { - UNREFERENCED_PARAMETER(index); - UNREFERENCED_PARAMETER(rootKey); - } - // Populates the index with the entries from MSIX. void PopulateIndexFromMSIX(SQLiteIndex& index) { @@ -57,7 +54,22 @@ namespace AppInstaller::Repository::Microsoft Utility::NormalizedString familyName = Utility::ConvertToUTF8(packageId.FamilyName()); manifest.Id = familyName; - manifest.Name = Utility::ConvertToUTF8(package.DisplayName()); + + // Attempt to get the DisplayName. Since this will retrieve the localized value, it has a chance to fail. + // Rather than completely skip this package in that case, we will simply fall back to using the package name below. + try + { + manifest.Name = Utility::ConvertToUTF8(package.DisplayName()); + } + catch (const winrt::hresult_error& hre) + { + AICLI_LOG(Repo, Info, << "winrt::hresult_error[0x" << Logging::SetHRFormat << hre.code() << ": " << + Utility::ConvertToUTF8(hre.message()) << "] exception thrown when getting DisplayName for " << familyName); + } + catch (...) + { + AICLI_LOG(Repo, Info, << "Unknown exception thrown when getting DisplayName for " << familyName); + } if (manifest.Name.empty()) { @@ -98,14 +110,26 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex index = SQLiteIndex::CreateNew(SQLITE_MEMORY_DB_CONNECTION_TARGET, Schema::Version::Latest()); // Put installed packages into the index + std::optional<ARPHelper> arpHelper; + if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_System) { - PopulateIndexFromARP(index, HKEY_LOCAL_MACHINE); + if (!arpHelper) + { + arpHelper = ARPHelper(); + } + + arpHelper->PopulateIndexFromARP(index, Manifest::ManifestInstaller::ScopeEnum::Machine); } if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::ARP_User) { - PopulateIndexFromARP(index, HKEY_CURRENT_USER); + if (!arpHelper) + { + arpHelper = ARPHelper(); + } + + arpHelper->PopulateIndexFromARP(index, Manifest::ManifestInstaller::ScopeEnum::User); } if (filter == PredefinedInstalledSourceFactory::Filter::None || filter == PredefinedInstalledSourceFactory::Filter::MSIX) diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -38,7 +38,7 @@ namespace AppInstaller::Repository::Microsoft SQLiteIndex& operator=(SQLiteIndex&&) = default; // Creates a new index database of the given version. - static SQLiteIndex CreateNew(const std::string& filePath, Schema::Version version); + static SQLiteIndex CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest()); // The disposition for opening the index. enum class OpenDisposition diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySearch.h @@ -114,6 +114,14 @@ namespace AppInstaller::Repository { // The InstallerType of an installed package InstalledType, + // The Scope of an installed package + InstalledScope, + // The system path where the package is installed + InstalledLocation, + // The standard uninstall command; which may be interactive + StandardUninstallCommand, + // An uninstall command that should be non-interactive + SilentUninstallCommand, }; // Convert a PackageVersionMetadata to a string. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -834,6 +834,10 @@ namespace AppInstaller::Repository switch (pvm) { case PackageVersionMetadata::InstalledType: return "InstalledType"sv; + case PackageVersionMetadata::InstalledScope: return "InstalledScope"sv; + case PackageVersionMetadata::InstalledLocation: return "InstalledLocation"sv; + case PackageVersionMetadata::StandardUninstallCommand: return "StandardUninstallCommand"sv; + case PackageVersionMetadata::SilentUninstallCommand: return "SilentUninstallCommand"sv; default: return "Unknown"sv; } } diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -6,6 +6,9 @@ #include <windows.h> #include <urlmon.h> +#include <wil/resource.h> +#include <wil/result_macros.h> + #include <AppInstallerDateTime.h> #include <AppInstallerDeployment.h> #include <AppInstallerDownloader.h> @@ -16,6 +19,7 @@ #include <AppInstallerSHA256.h> #include <AppInstallerStrings.h> #include <AppInstallerSynchronization.h> +#include <AppInstallerTelemetry.h> #include <AppInstallerVersions.h> #include <winget/ExtensionCatalog.h> #include <winget/ExperimentalFeature.h> @@ -23,8 +27,6 @@ #include <winget/UserSettings.h> #include <winget/Yaml.h> -#include <wil/result_macros.h> - #include <winsqlite/winsqlite3.h> #include <winrt/Windows.ApplicationModel.h> diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -11,19 +11,19 @@ using namespace std::filesystem; -std::wstring_view registrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; -std::wstring_view defaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; +std::string_view registrySubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; +std::string_view defaultProductID = "{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; -std::wstring GenerateUninstaller() { - path tempPath = temp_directory_path(); - path uninstallerPath = tempPath; +std::wstring GenerateUninstaller(std::ostream& out, const path& installDirectory) { + path uninstallerPath = installDirectory; uninstallerPath /= "UninstallTestExe.bat"; - std::cout << "Uninstaller located at path: " << uninstallerPath << '\n'; + out << "Uninstaller located at path: " << uninstallerPath << '\n'; - path uninstallerOutputTextFilePath = tempPath; + path uninstallerOutputTextFilePath = installDirectory; uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; + // TODO: Needs to re-invoke the installer and remove the Uninstall key that it added std::ofstream uninstallerScript(uninstallerPath); uninstallerScript << "@echo off\n"; uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; @@ -33,72 +33,80 @@ std::wstring GenerateUninstaller() { return uninstallerPath.wstring(); } -void WriteToUninstallRegistry(const std::wstring& productID, const std::wstring& uninstallerPath) { +void WriteToUninstallRegistry(std::ostream& out, const std::wstring& productID, const std::wstring& uninstallerPath) +{ HKEY hkey; LONG lReg; // String inputs to registry must be of wide char type - const wchar_t* displayName = L"AppInstallerTestExeInstaller\0"; - const wchar_t* publisher = L"Microsoft Corporation\0"; + const wchar_t* displayName = L"AppInstallerTestExeInstaller"; + const wchar_t* displayVersion = L"1.0.0.0"; + const wchar_t* publisher = L"Microsoft Corporation"; const wchar_t* uninstallString = uninstallerPath.c_str(); DWORD version = 1; - std::wstring registryKey = (std::wstring)registrySubkey; + path registryKey{ registrySubkey }; if (!productID.empty()) { - registryKey += productID; - std::wcout << "Product Code Overrided to: " << registryKey.c_str() << "\n"; + registryKey /= productID; + out << "Product Code overridden to: " << registryKey << "\n"; } else { - registryKey += defaultProductID; - std::wcout << "Default Product Code Used: " << registryKey.c_str() << "\n"; + registryKey /= defaultProductID; + out << "Default Product Code used: " << registryKey << "\n"; } lReg = RegCreateKeyEx( - HKEY_LOCAL_MACHINE, + HKEY_CURRENT_USER, registryKey.c_str(), 0, NULL, REG_OPTION_NON_VOLATILE, - KEY_ALL_ACCESS | KEY_WOW64_64KEY, + KEY_ALL_ACCESS, NULL, &hkey, NULL); if (lReg == ERROR_SUCCESS) { - std::cout << "Successfully opened registry key \n"; + out << "Successfully opened registry key \n"; // Set Display Name Property Value if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName, (DWORD)(wcslen(displayName) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { - std::cout << "Failed to write DisplayName value. Error Code: " << res << "\n"; + out << "Failed to write DisplayName value. Error Code: " << res << "\n"; + } + + // Set Display Version Property Value + if (LONG res = RegSetValueEx(hkey, L"DisplayVersion", NULL, REG_SZ, (LPBYTE)displayVersion, (DWORD)(wcslen(displayVersion) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + out << "Failed to write DisplayVersion value. Error Code: " << res << "\n"; } // Set Publisher Property Value if (LONG res = RegSetValueEx(hkey, L"Publisher", NULL, REG_SZ, (LPBYTE)publisher, (DWORD)(wcslen(publisher) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) { - std::cout << "Failed to write Publisher value. Error Code: " << res << "\n"; + out << "Failed to write Publisher value. Error Code: " << res << "\n"; } // Set UninstallString Property Value if (LONG res = RegSetValueEx(hkey, L"UninstallString", NULL, REG_EXPAND_SZ, (LPBYTE)uninstallString, (DWORD)wcslen(uninstallString + 1) * sizeof(wchar_t*)) != ERROR_SUCCESS) { - std::cout << "Failed to write UninstallString value. Error Code: " << res << "\n"; + out << "Failed to write UninstallString value. Error Code: " << res << "\n"; } // Set Version Property Value if (LONG res = RegSetValueEx(hkey, L"Version", NULL, REG_DWORD, (LPBYTE)&version, sizeof(version)) != ERROR_SUCCESS) { - std::cout << "Failed to write Version value. Error Code: " << res << "\n"; + out << "Failed to write Version value. Error Code: " << res << "\n"; } - std::cout << "Write to registry key completed \n"; + out << "Write to registry key completed \n"; } else { - std::cout << "Key Creation Failed\n"; + out << "Key Creation Failed\n"; } RegCloseKey(hkey); @@ -107,11 +115,15 @@ void WriteToUninstallRegistry(const std::wstring& productID, const std::wstring& // The installer prints all args to an output file and writes to the Uninstall registry key int main(int argc, const char** argv) { - path outFilePath = temp_directory_path(); + path installDirectory = temp_directory_path(); std::wstringstream productCodeStream; std::stringstream outContent; std::wstring productCode; + // Output to cout by default, but swap to a file if requested + std::ostream* out = &std::cout; + std::ofstream logFile; + for (int i = 1; i < argc; i++) { outContent << argv[i] << ' '; @@ -119,7 +131,7 @@ int main(int argc, const char** argv) // Supports custom install path. if (_stricmp(argv[i], "/InstallDir") == 0 && ++i < argc) { - outFilePath = argv[i]; + installDirectory = argv[i]; outContent << argv[i] << ' '; } @@ -128,8 +140,16 @@ int main(int argc, const char** argv) { productCodeStream << argv[i]; } + + // Supports log file + if (_stricmp(argv[i], "/LogFile") == 0 && ++i < argc) + { + logFile = std::ofstream(argv[i], std::ofstream::out | std::ofstream::trunc); + out = &logFile; + } } + path outFilePath = installDirectory; outFilePath /= "TestExeInstalled.txt"; std::ofstream file(outFilePath, std::ofstream::out); @@ -142,9 +162,9 @@ int main(int argc, const char** argv) productCode = productCodeStream.str(); } - std::wstring uninstallerPath = GenerateUninstaller(); + std::wstring uninstallerPath = GenerateUninstaller(*out, installDirectory); - WriteToUninstallRegistry(productCode, uninstallerPath); + WriteToUninstallRegistry(*out, productCode, uninstallerPath); return 0; }