From 72425326ccc5587db5618a73166e24833438f02d Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 30 May 2016 14:34:28 -0500 Subject: [PATCH] (GH-755) Track more MSI information Look up MSI in cryptic GUID location to gather more information when those things may be empty in uninstaller keys. Add localpackage location to know where the cached MSI is located. --- .../domain/RegistryApplicationKey.cs | 1 + .../services/RegistryService.cs | 147 +++++++++++++++++- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/chocolatey/infrastructure.app/domain/RegistryApplicationKey.cs b/src/chocolatey/infrastructure.app/domain/RegistryApplicationKey.cs index 5c36883024..f194ebe0ab 100644 --- a/src/chocolatey/infrastructure.app/domain/RegistryApplicationKey.cs +++ b/src/chocolatey/infrastructure.app/domain/RegistryApplicationKey.cs @@ -63,6 +63,7 @@ public class RegistryApplicationKey : IEquatable public bool NoRepair { get; set; } public string ReleaseType { get; set; } //hotfix, securityupdate, update rollup, servicepack public string ParentKeyName { get; set; } + public string LocalPackage { get; set; } /// /// Is an application listed in ARP (Programs and Features)? diff --git a/src/chocolatey/infrastructure.app/services/RegistryService.cs b/src/chocolatey/infrastructure.app/services/RegistryService.cs index d69805a368..e7e841ba30 100644 --- a/src/chocolatey/infrastructure.app/services/RegistryService.cs +++ b/src/chocolatey/infrastructure.app/services/RegistryService.cs @@ -16,12 +16,13 @@ namespace chocolatey.infrastructure.app.services { using System; - using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.AccessControl; using System.Security.Principal; + using System.Text; + using System.Text.RegularExpressions; using Microsoft.Win32; using domain; using filesystem; @@ -37,8 +38,10 @@ public sealed class RegistryService : IRegistryService private readonly IXmlService _xmlService; private readonly IFileSystem _fileSystem; private readonly bool _logOutput = false; + //public RegistryService() {} private const string UNINSTALLER_KEY_NAME = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + private const string UNINSTALLER_MSI_MACHINE_KEY_NAME = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Installer\\UserData"; private const string USER_ENVIRONMENT_REGISTRY_KEY_NAME = "Environment"; private const string MACHINE_ENVIRONMENT_REGISTRY_KEY_NAME = "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"; @@ -213,22 +216,27 @@ public void evaluate_keys(RegistryKey key, Registry snapshot) appKey.InstallerType = InstallerType.Custom; } + if (appKey.InstallerType == InstallerType.Msi) + { + get_msi_information(appKey, key); + } + if (_logOutput) { - if (appKey.is_in_programs_and_features() && appKey.InstallerType == InstallerType.Unknown) - { + //if (appKey.is_in_programs_and_features() && appKey.InstallerType == InstallerType.Unknown) + //{ foreach (var name in key.GetValueNames()) { //var kind = key.GetValueKind(name); var value = key.GetValue(name); if (name.is_equal_to("QuietUninstallString") || name.is_equal_to("UninstallString")) { - Console.WriteLine("key - {0}|{1}={2}|Type detected={3}".format_with(key.Name, name, value.to_string(), appKey.InstallerType.to_string())); + Console.WriteLine("key - {0}|{1}={2}|Type detected={3}|install location={4}".format_with(key.Name, name, value.to_string(), appKey.InstallerType.to_string(),appKey.InstallLocation.to_string())); } //Console.WriteLine("key - {0}, name - {1}, kind - {2}, value - {3}".format_with(key.Name, name, kind, value.to_string())); } - } + //} } snapshot.RegistryKeys.Add(appKey); @@ -238,6 +246,133 @@ public void evaluate_keys(RegistryKey key, Registry snapshot) key.Dispose(); } + private int _componentLoopCount = 0; + + private void get_msi_information(RegistryApplicationKey appKey, RegistryKey key) + { + _componentLoopCount = 0; + + var userDataProductKeyId = get_msi_user_data_key(key.Name); + if (string.IsNullOrWhiteSpace(userDataProductKeyId)) return; + + var hklm = open_key(RegistryHive.LocalMachine, RegistryView.Default); + if (Environment.Is64BitOperatingSystem) + { + hklm = open_key(RegistryHive.LocalMachine, RegistryView.Registry64); + } + + FaultTolerance.try_catch_with_logging_exception( + () => + { + var msiRegistryKey = hklm.OpenSubKey(UNINSTALLER_MSI_MACHINE_KEY_NAME, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey); + if (msiRegistryKey == null) return; + + foreach (var subKeyName in msiRegistryKey.GetSubKeyNames()) + { + var msiProductKey = FaultTolerance.try_catch_with_logging_exception( + () => msiRegistryKey.OpenSubKey("{0}\\Products\\{1}\\InstallProperties".format_with(subKeyName, userDataProductKeyId), RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), + "Failed to open subkey named '{0}' for '{1}', likely due to permissions".format_with(subKeyName, msiRegistryKey.Name), + logWarningInsteadOfError: true); + if (msiProductKey == null) continue; + + appKey.InstallLocation = set_if_empty(appKey.InstallLocation, msiProductKey.GetValue("InstallLocation").to_string()); + // informational + appKey.Publisher = set_if_empty(appKey.Publisher, msiProductKey.GetValue("Publisher").to_string()); + appKey.InstallDate = set_if_empty(appKey.InstallDate, msiProductKey.GetValue("InstallDate").to_string()); + appKey.InstallSource = set_if_empty(appKey.InstallSource, msiProductKey.GetValue("InstallSource").to_string()); + appKey.Language = set_if_empty(appKey.Language, msiProductKey.GetValue("Language").to_string()); + appKey.LocalPackage = set_if_empty(appKey.LocalPackage, msiProductKey.GetValue("LocalPackage").to_string()); + + // Version + appKey.DisplayVersion = set_if_empty(appKey.DisplayVersion, msiProductKey.GetValue("DisplayVersion").to_string()); + appKey.Version = set_if_empty(appKey.Version, msiProductKey.GetValue("Version").to_string()); + appKey.VersionMajor = set_if_empty(appKey.VersionMajor, msiProductKey.GetValue("VersionMajor").to_string()); + appKey.VersionMinor = set_if_empty(appKey.VersionMinor, msiProductKey.GetValue("VersionMinor").to_string()); + + // search components for install location if still empty + // the performance of this is very bad - without this the query is sub-second + // with this it takes about 15 seconds with around 200 apps installed + //if (string.IsNullOrWhiteSpace(appKey.InstallLocation) && !appKey.Publisher.contains("Microsoft")) + //{ + // var msiComponentsKey = FaultTolerance.try_catch_with_logging_exception( + // () => msiRegistryKey.OpenSubKey("{0}\\Components".format_with(subKeyName), RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), + // "Failed to open subkey named '{0}' for '{1}', likely due to permissions".format_with(subKeyName, msiRegistryKey.Name), + // logWarningInsteadOfError: true); + // if (msiComponentsKey == null) continue; + + // foreach (var msiComponentKeyName in msiComponentsKey.GetSubKeyNames()) + // { + // var msiComponentKey = FaultTolerance.try_catch_with_logging_exception( + // () => msiComponentsKey.OpenSubKey(msiComponentKeyName, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey), + // "Failed to open subkey named '{0}' for '{1}', likely due to permissions".format_with(subKeyName, msiRegistryKey.Name), + // logWarningInsteadOfError: true); + + // if (msiComponentKey.GetValueNames().Contains(userDataProductKeyId, StringComparer.OrdinalIgnoreCase)) + // { + // _componentLoopCount++; + // appKey.InstallLocation = set_if_empty(appKey.InstallLocation, get_install_location_estimate(msiComponentKey.GetValue(userDataProductKeyId).to_string())); + // if (!string.IsNullOrWhiteSpace(appKey.InstallLocation)) break; + // if (_componentLoopCount >= 10) break; + // } + // } + //} + } + }, + "Failed to open subkeys for '{0}', likely due to permissions".format_with(hklm.Name), + logWarningInsteadOfError: true); + } + + private string set_if_empty(string current, string proposed) + { + if (string.IsNullOrWhiteSpace(current)) return proposed; + + return current; + } + + private Regex _guidRegex = new Regex(@"\{(?\w*)\-(?\w*)\-(?\w*)\-(?\w*)\-(?\w*)\}", RegexOptions.Compiled); + private Regex _programFilesRegex = new Regex(@"(?\w)[\:\?]\\(?[Pp]rogram\s[Ff]iles|[Pp]rogram\s[Ff]iles\s\(x86\))\\(?:[Mm]icrosoft[^\\]*|[Cc]ommon\s[Ff]iles|IIS|MSBuild|[Rr]eference\s[Aa]ssemblies|[Ww]indows[^\\]*|(?[^\\]+))\\", RegexOptions.Compiled); + private StringBuilder _userDataKey = new StringBuilder(); + + private string get_install_location_estimate(string componentPath) + { + var match = _programFilesRegex.Match(componentPath); + if (!match.Success) return string.Empty; + if (string.IsNullOrWhiteSpace(match.Groups["Name"].Value)) return string.Empty; + + return "{0}:\\{1}\\{2}".format_with(match.Groups["Drive"].Value, match.Groups["ProgFiles"].Value, match.Groups["Name"].Value); + } + + private string get_msi_user_data_key(string name) + { + _userDataKey.Clear(); + var match = _guidRegex.Match(name); + if (!match.Success) return string.Empty; + + for (int i = 0; i < 3; i++) + { + var fullGroup = match.Groups["ReverseFull{0}".format_with(i + 1)]; + if (fullGroup != null) + { + _userDataKey.Append(fullGroup.Value.ToCharArray().Reverse().ToArray()); + } + } + for (int i = 0; i < 2; i++) + { + var pairsGroup = match.Groups["ReversePairs{0}".format_with(i + 1)]; + if (pairsGroup != null) + { + var pairValue = pairsGroup.Value; + for (int j = 0; j < pairValue.Length - 1; j++) + { + _userDataKey.Append("{1}{0}".format_with(pairValue[j], pairValue[j + 1])); + j++; + } + } + } + + return _userDataKey.to_string(); + } + public Registry get_installer_key_differences(Registry before, Registry after) { //var difference = after.RegistryKeys.Where(r => !before.RegistryKeys.Contains(r)).ToList(); @@ -368,7 +503,7 @@ public static GenericRegistryValue get_value(RegistryHiveType hive, string subKe value = FaultTolerance.try_catch_with_logging_exception( () => { - if (key.GetValueNames().Contains(registryValue,StringComparer.InvariantCultureIgnoreCase)) + if (key.GetValueNames().Contains(registryValue, StringComparer.InvariantCultureIgnoreCase)) { return new GenericRegistryValue {