Ejemplo n.º 1
0
        /// <summary>
        /// Determines the state of the specified product.
        /// </summary>
        /// <param name="productCode">The product code of the MSI to detect.</param>
        /// <param name="installedVersion">If detected, contains the version of the installed MSI.</param>
        /// <returns>The detect state of the specified MSI.</returns>
        private DetectState Detect(string productCode, out string installedVersion)
        {
            uint error = WindowsInstaller.GetProductInfo(productCode, InstallProperty.VERSIONSTRING, out installedVersion);

            DetectState state = error == Error.SUCCESS ? DetectState.Present
                : (error == Error.UNKNOWN_PRODUCT) || (error == Error.UNKNOWN_PROPERTY) ? DetectState.Absent
                : DetectState.Unknown;

            ExitOnError(state == DetectState.Unknown, error, $"Failed to detect MSI package with ProductCode={productCode}.");
            Log?.LogMessage($"Detected package, ProductCode: {productCode}, version: {(string.IsNullOrWhiteSpace(installedVersion) ? "n/a" : $"{installedVersion}")}, state: {state}.");
Ejemplo n.º 2
0
        /// <summary>
        /// Determines the state of the specified product.
        /// </summary>
        /// <param name="productCode">The product code of the MSI to detect.</param>
        /// <param name="installedVersion">If detected, contains the version of the installed MSI.</param>
        /// <returns>The detect state of the specified MSI.</returns>
        private DetectState DetectPackage(string productCode, out Version installedVersion)
        {
            installedVersion = default;
            uint error = WindowsInstaller.GetProductInfo(productCode, InstallProperty.VERSIONSTRING, out string versionValue);

            DetectState state = error == Error.SUCCESS ? DetectState.Present
                : (error == Error.UNKNOWN_PRODUCT) || (error == Error.UNKNOWN_PROPERTY) ? DetectState.Absent
                : DetectState.Unknown;

            ExitOnError(state == DetectState.Unknown, error, $"DetectPackage: Failed to detect MSI package, ProductCode: {productCode}.");

            if (state == DetectState.Present)
            {
                if (!Version.TryParse(versionValue, out installedVersion))
                {
                    Log?.LogMessage($"DetectPackage: Failed to parse version: {versionValue}.");
                }
            }

            Log?.LogMessage($"DetectPackage: ProductCode: {productCode}, version: {installedVersion?.ToString() ?? "n/a"}, state: {state}.");

            return(state);
        }
Ejemplo n.º 3
0
        /// <summary>
        /// Plans the specified MSI payload based on its state and the requested install action.
        /// </summary>
        /// <param name="msi">The MSI package to plan.</param>
        /// <param name="state">The detected state of the package.</param>
        /// <param name="requestedAction">The requested action to perform.</param>
        /// <returns>The action that will be performed.</returns>
        private InstallAction PlanPackage(MsiPayload msi, DetectState state, InstallAction requestedAction, Version installedVersion, out IEnumerable <string> relatedProductCodes)
        {
            InstallAction    plannedAction   = InstallAction.None;
            HashSet <string> relatedProducts = new();

            Log?.LogMessage($"PlanPackage: Begin, name: {msi.Name}, version: {msi.ProductVersion}, state: {state}, installed version: {installedVersion?.ToString() ?? "n/a"}, requested: {requestedAction}.");

            // Manifest packages should support major upgrades (both the ProductCode and ProductVersion should be different) while
            // workload packs should always be SxS (ProductCode and Upgrade should be different for each version).
            //
            // We cannot discount someone generating a minor update (ProductCode remains unchanged, but ProductVersion changes),
            // so we'll detect downgrades and minor updates. For more details, see https://docs.microsoft.com/en-us/windows/win32/msi/minor-upgrades.
            if (state == DetectState.Present)
            {
                if (msi.ProductVersion < installedVersion)
                {
                    Log?.LogMessage($"PlanPackage: Downgrade detected, installed version: {installedVersion}, requested version: {msi.ProductVersion}.");
                    plannedAction = InstallAction.Downgrade;
                    state         = DetectState.Superseded;
                }
                else if (msi.ProductVersion > installedVersion)
                {
                    Log?.LogMessage($"PlanPackage: Minor update detected, installed version: {installedVersion}, requested version: {msi.ProductVersion}.");
                    plannedAction = InstallAction.MinorUpdate;
                    state         = DetectState.Obsolete;
                }
                else
                {
                    // If the package is installed, then we can uninstall and repair it.
                    plannedAction = (requestedAction != InstallAction.Repair) && (requestedAction != InstallAction.Uninstall) ? InstallAction.None : requestedAction;
                }
            }
            else if (state == DetectState.Absent)
            {
                // If we're absent, convert repair to install or keep install.
                plannedAction = (requestedAction == InstallAction.Repair) ? InstallAction.Install
                    : (requestedAction == InstallAction.Install) ? InstallAction.Install
                    : InstallAction.None;
            }

            // At this point we know the MSI is absent so there are only three outcomes when executing the package:
            //   1. We'll just do a clean install if we don't find related products so we're either brand new or SxS.
            //   2. We'll perform a major upgrade.
            //   3. We'll trigger a downgrade and likely an error since most MSIs detect and block downgrades.
            //
            // We'll process the related product information to make a determination. This is similar to what the FindRelatedProducts
            // action does when an MSI is executed.
            foreach (RelatedProduct relatedProduct in msi.RelatedProducts)
            {
                foreach (string relatedProductCode in WindowsInstaller.FindRelatedProducts(relatedProduct.UpgradeCode))
                {
                    // Ignore potentially detecting ourselves.
                    if (string.Equals(relatedProductCode, msi.ProductCode, StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }

                    // Check whether the related product is installed and retrieve its version to determine
                    // how we're related.
                    uint error = WindowsInstaller.GetProductInfo(relatedProductCode, InstallProperty.VERSIONSTRING, out string relatedVersionValue);

                    // Continue searching if the related product is not installed.
                    if (error == Error.UNKNOWN_PRODUCT || error == Error.UNKNOWN_PROPERTY)
                    {
                        continue;
                    }

                    ExitOnError(error, $"PlanPackage: Failed to retrieve version for related product: ProductCode: {relatedProductCode}.");

                    // Parse the version, but don't try to catch any errors. If the version is invalid we want to fail
                    // because we can't compare invalid versions to see whether it's excluded by the VersionMin and VersionMax
                    // columns from the Upgrade table.
                    Version relatedVersion = Version.Parse(relatedVersionValue);

                    if (relatedProduct.ExcludesMinVersion(relatedVersion) || relatedProduct.ExcludesMaxVersion(relatedVersion))
                    {
                        continue;
                    }

                    // Check if the related product contains a matching language code (LCID). If we don't have any languages,
                    // all languages are detectable as related and we can ignore the msidbUpgradeAttributesLanguagesExclusive flag.
                    if (relatedProduct.Languages.Any())
                    {
                        string relatedLanguage = "0";
                        error = WindowsInstaller.GetProductInfo(relatedProductCode, InstallProperty.LANGUAGE, out relatedLanguage);

                        if (int.TryParse(relatedLanguage, out int lcid))
                        {
                            if (relatedProduct.ExcludesLanguage(lcid))
                            {
                                continue;
                            }
                        }
                        else
                        {
                            Log?.LogMessage($"PlanPackage: Failed to read Language property for related product, ProductCode: {relatedProductCode}. The related product will be skipped.");
                            continue;
                        }
                    }

                    relatedProducts.Add(relatedProductCode);
                    plannedAction = InstallAction.MajorUpgrade;

                    if (relatedProduct.Attributes.HasFlag(UpgradeAttributes.OnlyDetect) && (state == DetectState.Absent))
                    {
                        // If we're not installed, but detect-only related, it's very likely that
                        // that we'd trigger a downgrade launch condition. We can't know for sure, but
                        // generally that's the most common use for detect-only entries.
                        plannedAction = InstallAction.Downgrade;
                    }

                    Log?.LogMessage($"PlanPackage: Detected related product, ProductCode: {relatedProductCode}, version: {relatedVersion}, attributes: {relatedProduct.Attributes}, planned action: {plannedAction}.");
                }
            }

            Log?.LogMessage($"PlanPackage: Completed, name: {msi.Name}, version: {msi.ProductVersion}, state: {state}, installed version: {installedVersion?.ToString() ?? "n/a"}, requested: {requestedAction}, planned: {plannedAction}.");

            relatedProductCodes = relatedProducts.Select(p => p);
            return(plannedAction);
        }
        public void InstallProductReturnsAnError(string productCode, string property, uint expectedError)
        {
            uint error = WindowsInstaller.GetProductInfo(productCode, property, out string propertyValue);

            Assert.Equal(error, expectedError);
        }