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

Use UpgradeCode for matching MSI apps #2418

Merged
merged 16 commits into from Aug 15, 2022
2 changes: 2 additions & 0 deletions .github/actions/spelling/allow.txt
Expand Up @@ -3,6 +3,7 @@ ACCESSDENIED
ACTIONDATA
ACTIONSTART
addmanifest
addressof
addstore
admins
alloc
Expand Down Expand Up @@ -318,6 +319,7 @@ msdata
MSDN
msi
msiexec
MSIHANDLE
msix
msixbundle
msixinfo
Expand Down
3 changes: 2 additions & 1 deletion .github/actions/spelling/expect.txt
Expand Up @@ -302,9 +302,9 @@ ofile
openmode
Outptr
packageinuse
PACL
PARAMETERMAP
paramref
PACL
pathparts
Patil
pb
Expand Down Expand Up @@ -444,6 +444,7 @@ unparsable
UNSCOPED
unvirtualized
UParse
upgradecode
UPSERT
uris
URLs
Expand Down
8 changes: 8 additions & 0 deletions src/AppInstallerCLICore/Workflows/InstallFlow.cpp
Expand Up @@ -691,6 +691,14 @@ namespace AppInstaller::CLI::Workflow
entries.push_back(std::move(entry));
}

auto upgradeCodes = correlationResult.Package->GetMultiProperty(PackageVersionMultiProperty::UpgradeCode);
for (auto&& upgradeCode : upgradeCodes)
{
AppsAndFeaturesEntry entry = baseEntry;
entry.UpgradeCode= std::move(upgradeCode).get();
entries.push_back(std::move(entry));
}

context.Add<Data::CorrelatedAppsAndFeaturesEntries>(std::move(entries));
}

Expand Down
19 changes: 19 additions & 0 deletions src/AppInstallerCLIE2ETests/ListCommand.cs
Expand Up @@ -57,6 +57,25 @@ public void ListWithArpVersionMapping()
ArpVersionMappingTest("AppInstallerTest.TestArpVersionSameOrder", "TestArpVersionSameOrder", "12.0", "> 2.0", "12.0");
}

[Test]
public void ListWithUpgradeCode()
{
// Installs the MSI installer using the TestMsiInstaller package.
// Then tries listing the TestMsiInstallerUpgradeCode package, which should
// be correlated to it by the UpgradeCode.
if (string.IsNullOrEmpty(TestCommon.MsiInstallerPath))
{
Assert.Ignore("MSI installer not available");
}

var installDir = TestCommon.GetRandomTestDir();
Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("install", $"TestMsiInstaller --silent -l {installDir}").ExitCode);

var result = TestCommon.RunAICLICommand("list", "TestMsiInstallerUpgradeCode");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("AppInstallerTest.TestMsiInstallerUpgradeCode"));
}

private void ArpVersionMappingTest(string packageIdentifier, string displayNameOverride, string displayVersionOverride, string expectedListVersion, string notExpectedListVersion = "")
{
System.Guid guid = System.Guid.NewGuid();
Expand Down
@@ -0,0 +1,15 @@
# Uses the MSI installer; doesn't list the ProductCode, only the UpgradeCode
PackageIdentifier: AppInstallerTest.TestMsiInstallerUpgradeCode
PackageVersion: 1.0.0.0
PackageLocale: en-US
PackageName: TestMsiInstallerUpgradeCode
Publisher: AppInstallerTest
Installers:
- Architecture: x86
InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestMsiInstaller/AppInstallerTestMsiInstaller.msi
InstallerType: msi
InstallerSha256: <MSIHASH>
AppsAndFeaturesEntries:
- UpgradeCode: '{B9CF9DD5-D46F-4CE0-BFC9-633BF9D3A6F4}'
ManifestType: singleton
ManifestVersion: 1.1.0
17 changes: 14 additions & 3 deletions src/AppInstallerRepositoryCore/ARPCorrelation.cpp
Expand Up @@ -188,15 +188,16 @@ namespace AppInstaller::Repository::Correlation
}
}

std::vector<std::string> productCodes;
std::set<std::string> productCodes;
std::set<std::string> upgradeCodes;
for (const auto& installer : manifest.Installers)
{
if (!installer.ProductCode.empty())
{
if (std::find(productCodes.begin(), productCodes.end(), installer.ProductCode) == productCodes.end())
// Add each ProductCode only once
if (productCodes.insert(installer.ProductCode).second)
{
manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, installer.ProductCode));
productCodes.emplace_back(installer.ProductCode);
}
}

Expand All @@ -208,6 +209,16 @@ namespace AppInstaller::Repository::Correlation
appsAndFeaturesEntry.DisplayName,
appsAndFeaturesEntry.Publisher.empty() ? defaultPublisher : appsAndFeaturesEntry.Publisher));
}

// Add each ProductCode and UpgradeCode only once;
if (!appsAndFeaturesEntry.ProductCode.empty() && upgradeCodes.insert(appsAndFeaturesEntry.ProductCode).second)
{
manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, appsAndFeaturesEntry.ProductCode));
}
if (!appsAndFeaturesEntry.UpgradeCode.empty() && upgradeCodes.insert(appsAndFeaturesEntry.UpgradeCode).second)
{
manifestSearchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::UpgradeCode, MatchType::Exact, appsAndFeaturesEntry.UpgradeCode));
}
}
}

Expand Down
Expand Up @@ -269,6 +269,9 @@
<ClInclude Include="Microsoft\Schema\1_4\Interface.h" />
<ClInclude Include="Microsoft\Schema\1_5\ArpVersionVirtualTable.h" />
<ClInclude Include="Microsoft\Schema\1_5\Interface.h" />
<ClInclude Include="Microsoft\Schema\1_6\Interface.h" />
<ClInclude Include="Microsoft\Schema\1_6\SearchResultsTable.h" />
<ClInclude Include="Microsoft\Schema\1_6\UpgradeCodeTable.h" />
<ClInclude Include="Microsoft\Schema\ISQLiteIndex.h" />
<ClInclude Include="Microsoft\Schema\MetadataTable.h" />
<ClInclude Include="Microsoft\Schema\Version.h" />
Expand Down Expand Up @@ -344,6 +347,8 @@
<ClCompile Include="Microsoft\Schema\1_4\DependenciesTable.cpp" />
<ClCompile Include="Microsoft\Schema\1_4\Interface_1_4.cpp" />
<ClCompile Include="Microsoft\Schema\1_5\Interface_1_5.cpp" />
<ClCompile Include="Microsoft\Schema\1_6\Interface_1_6.cpp" />
<ClCompile Include="Microsoft\Schema\1_6\SearchResultsTable_1_6.cpp" />
<ClCompile Include="Microsoft\Schema\MetadataTable.cpp" />
<ClCompile Include="Microsoft\Schema\Version.cpp" />
<ClCompile Include="Microsoft\SQLiteIndex.cpp" />
Expand Down
Expand Up @@ -64,6 +64,9 @@
<Filter Include="Microsoft\Schema\1_5">
<UniqueIdentifier>{e31c8e5b-ed2c-43c8-b91b-db8ec4c52f71}</UniqueIdentifier>
</Filter>
<Filter Include="Microsoft\Schema\1_6">
<UniqueIdentifier>{84a55def-9fb8-4c90-8d5a-2cedc171940b}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
Expand Down Expand Up @@ -288,6 +291,15 @@
<ClInclude Include="Microsoft\Schema\1_0\VirtualTableBase.h">
<Filter>Microsoft\Schema\1_0</Filter>
</ClInclude>
<ClInclude Include="Microsoft\Schema\1_6\Interface.h">
<Filter>Microsoft\Schema\1_6</Filter>
</ClInclude>
<ClInclude Include="Microsoft\Schema\1_6\UpgradeCodeTable.h">
<Filter>Microsoft\Schema\1_6</Filter>
</ClInclude>
<ClInclude Include="Microsoft\Schema\1_6\SearchResultsTable.h">
<Filter>Microsoft\Schema\1_6</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
Expand Down Expand Up @@ -452,6 +464,12 @@
<ClCompile Include="ArpVersionValidation.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Microsoft\Schema\1_6\Interface_1_6.cpp">
<Filter>Microsoft\Schema\1_6</Filter>
</ClCompile>
<ClCompile Include="Microsoft\Schema\1_6\SearchResultsTable_1_6.cpp">
<Filter>Microsoft\Schema\1_6</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
Expand Down
7 changes: 7 additions & 0 deletions src/AppInstallerRepositoryCore/CompositeSource.cpp
Expand Up @@ -24,6 +24,7 @@ namespace AppInstaller::Repository
{
case AppInstaller::Repository::PackageMatchField::PackageFamilyName:
case AppInstaller::Repository::PackageMatchField::ProductCode:
case AppInstaller::Repository::PackageMatchField::UpgradeCode:
return true;
}

Expand Down Expand Up @@ -738,6 +739,12 @@ namespace AppInstaller::Repository
PackageMatchField::ProductCode,
data);

GetSystemReferenceStrings(
version,
PackageVersionMultiProperty::UpgradeCode,
PackageMatchField::UpgradeCode,
data);

GetNameAndPublisher(
version,
data);
Expand Down
72 changes: 63 additions & 9 deletions src/AppInstallerRepositoryCore/Microsoft/ARPHelper.cpp
Expand Up @@ -3,11 +3,57 @@
#include "pch.h"
#include "ARPHelper.h"
#include "winget/PortableARPEntry.h"
#include <Msi.h>
florelis marked this conversation as resolved.
Show resolved Hide resolved

namespace AppInstaller::Repository::Microsoft
{
using namespace AppInstaller::Registry::Portable;

namespace
{
// Finds the UpgradeCode corresponding to a given ProductCode
std::optional<std::string> GetUpgradeCodeByProductCode(const std::string& productCode)
{
// The UpgradeCode is not stored in the ARP registry keys, so we use the MSI API to query it.
//
// The UpgradeCode is apparently also stored in the registry under
// HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes\<UpgradeCode>
// but:
// * This key does not seem to be documented
// * The format used to store the GUIDs is harder to parse
// * We cannot look up by ProductCode; we need to take an UpgradeCode and then find the ProductCode
// Using the registry could potentially be faster, so we may consider using it if this proves to be too slow.
florelis marked this conversation as resolved.
Show resolved Hide resolved

// For some reason MsiOpenProduct pops up a dialog.
// Suppress it by setting the UI level to None.
MsiSetInternalUI(INSTALLUILEVEL_NONE, nullptr);

wil::unique_any<MSIHANDLE, decltype(MsiCloseHandle), MsiCloseHandle> msiProduct;
if (SUCCEEDED(MsiOpenProductA(productCode.c_str(), msiProduct.addressof())))
{
const auto UpgradeCodePropertyName = "UpgradeCode";

// This functions returns success and sets the length of the property value, excluding null terminator
DWORD upgradeCodeLength = 0;
if (SUCCEEDED(MsiGetProductPropertyA(msiProduct.get(), UpgradeCodePropertyName, nullptr, &upgradeCodeLength)))
{
// Create a buffer of the appropriate size
std::string upgradeCode(static_cast<size_t>(upgradeCodeLength), ' ');

// +1 because the function expects us to count the null terminator
upgradeCodeLength = static_cast<DWORD>(upgradeCode.size() + 1);
if (SUCCEEDED(MsiGetProductPropertyA(msiProduct.get(), UpgradeCodePropertyName, upgradeCode.data(), &upgradeCodeLength)))
{
return upgradeCode;
}
}
}

// UpgradeCode not found
return {};
}
}

Registry::Key ARPHelper::GetARPKey(Manifest::ScopeEnum scope, Utility::Architecture architecture) const
{
HKEY rootKey = NULL;
Expand Down Expand Up @@ -294,6 +340,23 @@ namespace AppInstaller::Repository::Microsoft
//manifest.Id = normalizedName.Publisher() + '.' + normalizedName.Name();
}

// 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::InstallerTypeEnum::Exe;

if (GetBoolValue(arpKey, WindowsInstaller))
{
installedType = Manifest::InstallerTypeEnum::Msi;

// If this is an MSI, look up the UpgradeCode
auto upgradeCode = GetUpgradeCodeByProductCode(productCode);
if (upgradeCode)
{
manifest.Installers[0].AppsAndFeaturesEntries.emplace_back();
manifest.Installers[0].AppsAndFeaturesEntries[0].UpgradeCode = *upgradeCode;
}
}

// 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.

Expand Down Expand Up @@ -347,15 +410,6 @@ namespace AppInstaller::Repository::Microsoft
// Pick up Language to enable proper selection of language for upgrade.
AddMetadataIfPresent(arpKey, Language, index, manifestId, PackageVersionMetadata::InstalledLocale);

// 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::InstallerTypeEnum::Exe;

if (GetBoolValue(arpKey, WindowsInstaller))
{
installedType = Manifest::InstallerTypeEnum::Msi;
}

if (Manifest::ConvertToInstallerTypeEnum(GetStringValue(arpKey, std::wstring{ ToString(PortableValueName::WinGetInstallerType) })) == Manifest::InstallerTypeEnum::Portable)
{
// Portable uninstall requires the installed architecture for locating the entry in the registry.
Expand Down
Expand Up @@ -29,25 +29,36 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1
{
std::vector<Utility::NormalizedString> GetSystemReferenceStrings(
const Manifest::Manifest& manifest,
std::function<const Utility::NormalizedString&(const Manifest::ManifestInstaller&)> func)
std::function<const Utility::NormalizedString&(const Manifest::ManifestInstaller&)> extractStringFromInstaller,
std::optional<std::function<const Utility::NormalizedString& (const Manifest::AppsAndFeaturesEntry&)>> extractStringFromAppsAndFeaturesEntry = {})
florelis marked this conversation as resolved.
Show resolved Hide resolved
{
std::set<Utility::NormalizedString> set;

for (const auto& installer : manifest.Installers)
{
const Utility::NormalizedString& string = func(installer);
if (!string.empty())
const auto& installerString = extractStringFromInstaller(installer);
if (!installerString.empty())
{
set.emplace(Utility::FoldCase(string));
set.emplace(Utility::FoldCase(installerString));
}
}

std::vector<Utility::NormalizedString> result;
for (auto&& string : set)
{
result.emplace_back(string);
if (extractStringFromAppsAndFeaturesEntry.has_value())
{
for (const auto& entry : installer.AppsAndFeaturesEntries)
{
const auto& entryString = (*extractStringFromAppsAndFeaturesEntry)(entry);
if (!entryString.empty())
{
set.emplace(Utility::FoldCase(entryString));
}
}
}
}

std::vector<Utility::NormalizedString> result(
std::make_move_iterator(set.begin()),
std::make_move_iterator(set.end()));

return result;
}

Expand All @@ -58,7 +69,10 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1

std::vector<Utility::NormalizedString> GetProductCodes(const Manifest::Manifest& manifest)
{
return GetSystemReferenceStrings(manifest, [](const Manifest::ManifestInstaller& i) -> const Utility::NormalizedString& { return i.ProductCode; });
return GetSystemReferenceStrings(
manifest,
[](const Manifest::ManifestInstaller& i) -> const Utility::NormalizedString& { return i.ProductCode; },
[](const Manifest::AppsAndFeaturesEntry& e) -> const Utility::NormalizedString& { return e.ProductCode; });
}
}

Expand Down Expand Up @@ -216,7 +230,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_1

std::unique_ptr<V1_0::SearchResultsTable> Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const
{
return std::make_unique<SearchResultsTable>(connection);
return std::make_unique<V1_1::SearchResultsTable>(connection);
}

void Interface::PerformQuerySearch(V1_0::SearchResultsTable& resultsTable, const RequestMatch& query) const
Expand Down
Expand Up @@ -207,7 +207,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_2

std::unique_ptr<V1_0::SearchResultsTable> Interface::CreateSearchResultsTable(const SQLite::Connection& connection) const
{
return std::make_unique<SearchResultsTable>(connection);
return std::make_unique<V1_2::SearchResultsTable>(connection);
}

ISQLiteIndex::SearchResult Interface::SearchInternal(const SQLite::Connection& connection, SearchRequest& request) const
Expand Down
Expand Up @@ -17,7 +17,7 @@ namespace AppInstaller::Repository::Microsoft::Schema::V1_3
void CreateTables(SQLite::Connection& connection, CreateOptions options) override;
SQLite::rowid_t AddManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional<std::filesystem::path>& relativePath) override;
std::pair<bool, SQLite::rowid_t> UpdateManifest(SQLite::Connection& connection, const Manifest::Manifest& manifest, const std::optional<std::filesystem::path>& relativePath) override;

protected:
// Gets a property already knowing that the manifest id is valid.
std::optional<std::string> GetPropertyByManifestIdInternal(const SQLite::Connection& connection, SQLite::rowid_t manifestId, PackageVersionProperty property) const override;
Expand Down