Web PI APIs: Download Latest Web Stack Products for Any Platform
Suppose you have a central Internet faced server and multiple backend servers not connected to the Internet. To insure that your Internet faced server is always up-to-date with the latest Microsoft Web Stack products and updates, you can use Web Platform Installer. You can either use Web PI UI to set up your main server or you can automate the setup process by interfacing with Web PI APIs through Microsoft.Web.PlatformInstaller.dll. It was already shown in my previous blog post how to load and install specific products for the current machine through the APIs. In this blog, I will show how to download products for any Web PI supported platform using APIs. This solution might help you in setting up your load balancer that can download and cache products for its backend servers regardless of their OS and architecture. This solution is by no means perfect and final and is given as a sample.
Overview
From this post you will learn how to use WebPI APIs to:
- load the main product and application feeds
- filter products by any OS and architecture
- pull products with their dependencies
- manage installers locally in the installer cache
- avoid a dependency check for downloaded products
Details
When dealing with Web PI APIs, you will deal mostly with ProductManager and InstallManager classes. ProductManager is responsible for loading and filtering product information from the xml feeds, where all product information is stored. And InstallManager is responsible for actually downloading and installing the products (and applications, and updates – basically everything that is available through the Web PI feeds).
ProductManager has several overloads for Load method that loads the main feed with all its enclosures (other product feeds like application feed). The overload that will be used in the sample accepts more parameters than his other overloaded counterparts. With this overload you can make Web PI filter products by any OS and architecture. If you use the overload that does not accept parameters for the OS and architecture, it means that Web PI will filter products by version, type and architecture of your current platform. Thus, for the overload that we will be using, you will need to specify the architecture (x86 or x64), the major and minor OS versions, major and minor SP versions and OS type. Here is a small table that shows the major and minor versions of some OSes.
OS | Major Version | Minor Version |
Windows 7/ Windows Server 2008 R2 | 6 | 1 |
Vista/Windows Server 2008 | 6 | 0 |
Windows Server 2003 | 5 | 2 |
Windows XP | 5 | 1 |
Finally, ProductManager can find you a Product based on its ProductId (can be looked up directly in the xml feed), which will contain all information available for this product including information about its installers. From a product object you just need to pick the right installer you want to use to install this product. The right installer might be based not only on the specific platform you want to install this product on, but also on the language of the installers, if the product is available in that language.
InstallManager can cache products without installing them. The only catch in this case is that while loading products (ProductManager.Load), Web PI will perform a dependency check for the products you are trying to load against your current platform and will throw an exception if you try to load something incompatible with your current platform. That’s fine. You can pragmatically download the installers yourself using the links that each installer object carries along (Installer.InstallerFile.InstallerUrl). The disadvantage of downloading the files yourself is that you don’t use the following goodies that Web PI provides you with in the background when downloading the installers itself:
- installer management in central location, aka Installer Cache (%LocalAppData%\Microsoft\Web Platform Installer\installers) with different paths for different installers of the same product (based on the installer signature)
- order of installers – Web PI installs products in a smart way, because some products have to be installed after their dependencies are installed
- retry logic for download failures
- figuring out the installer’s extension (can be msi or exe or msu (for updates), etc)
On the other hand, the advantage of using Web PI APIs in this case is still great. You are guaranteed to get "the latest and greatest" products and updates in one central location and Web PI provides you with powerful APIs to leverage this feature in the easy-to-use way that can also be automated!
Take a look yourself…
Code
You can grab the full project (with only this file in it) here.
using System; using System.Collections.Generic; using System.IO; using System.Net; using Microsoft.Web.PlatformInstaller; namespace ProductCacher { class Program { protected readonly static string MainXml = "https://go.microsoft.com/fwlink/?LinkId=158722"; static void Main(string[] args) { ProductManager productManager = new ProductManager(); // 1. Load main product feeds // overload documentation: http://msdn.microsoft.com/en-us/library/ee255981.aspx productManager.Load ( new Uri(MainXml), // location of the main feed false, // do not filter products by the current architecture and OS true, // load enclosures (eg WebApplicationList.xml) false, // no cache yet, so do not use cached directory @"d:\MyCache", // where to cache XML feeds (the folder should exist) // get only products available for // a. x86 architecture (and those available for both x86 AND x64) // b. the current OS (major and minor OS versions) // c. up to service pack 2 (major and minor sevice pack versions) // d. any product types (e.g. Home, Professional, Ultimate, Web Server, etc) Architecture.x86, // arch Environment.OSVersion.Version.Major, // current major version Environment.OSVersion.Version.Minor, // current minor version 2, // current major SP 0, // current minor SP 0 // OsType as defined in this list: http://msdn.microsoft.com/en-us/library/ms724358(VS.85).aspx // 0 if don't want to filter products by OSType ); // 2. Find all the products to cache: .NET Fx 3.5 SP1, SQL Expess, PHP, MySQL, UrlRewrite and SEOToolkit Product netFramework35Sp1 = productManager.GetProduct("NETFramework35"); Product sqlExpress = productManager.GetProduct("SQLExpress"); Product php = productManager.GetProduct("PHP"); Product mySql = productManager.GetProduct("MySQL"); Product SEOtoolkit = productManager.GetProduct("SEOToolkit"); Product urlRewrite = productManager.GetProduct("UrlRewrite"); Product webDeploy = productManager.GetProduct("WDeploy"); // 3. Add all products with the dependencies to the install list List<Product> productsToInstall = new List<Product>(); AddProductWithDependencies(netFramework35Sp1, productsToInstall); AddProductWithDependencies(sqlExpress, productsToInstall); AddProductWithDependencies(php, productsToInstall); AddProductWithDependencies(mySql, productsToInstall); AddProductWithDependencies(SEOtoolkit, productsToInstall); AddProductWithDependencies(urlRewrite, productsToInstall); AddProductWithDependencies(webDeploy, productsToInstall); // 4. For each product pick the installers (can choose a language of each installer if localized installer is available) List<Installer> installersToUse = new List<Installer>(); Language english = productManager.GetLanguage("en"); foreach (Product productToInstall in productsToInstall) { Installer currentInstaller = productToInstall.GetInstaller(english); // if current product has installer to download (it might be an installer-less Windows component) if (currentInstaller != null && currentInstaller.InstallerFile != null) { installersToUse.Add(currentInstaller); } } // ***********************************************************************// // NOTE: if you use this code, to make WebPI download the installers, // WebPI will perform a dependency check against your CURRENT platform. // For example, if you are trying to download something incompatible with your // current OS, WebPI will throw // ***********************************************************************// /* // 5. Prepare installers InstallManager installManager = new InstallManager(); installManager.Load(installersToUse); // 6. Download products without installing them foreach (InstallerContext installerContext in installManager.InstallerContexts) { // each installer will be cached under %LocalAppData%\Microsoft\Web Platform Installer\installers\<product Id>\<installer's SHA1>\ string failureReason; installManager.DownloadInstallerFile(installerContext, out failureReason); if (!string.IsNullOrEmpty(failureReason)) { Console.Error.WriteLine("Web PI download failure: " + failureReason); } } // ***********************************************************************/ // 5. Download the installers in the same folder Web PI does, aka Installer Cache string WebPIinstallersFolder = Environment.ExpandEnvironmentVariables(@"%LocalAppData%\Microsoft\Web Platform Installer\installers"); if (!Directory.Exists(WebPIinstallersFolder)) { Directory.CreateDirectory(WebPIinstallersFolder); } foreach (Installer installer in installersToUse) { // re-create WebPI behavior: download each installer // under %LocalAppData%\Microsoft\Web Platform Installer\installers\<product Id>\<installer's SHA1>\ string currentInstallerDirectory = Path.Combine(WebPIinstallersFolder, installer.Product.ProductId) + "\\" + installer.InstallerFile.SHA1Hash; // if the directory <productId>\<installer's hash> doesn't exist, create it if (!Directory.Exists(currentInstallerDirectory)) { Directory.CreateDirectory(currentInstallerDirectory); } WebClient downloadClient = new WebClient(); downloadClient.DownloadFile ( installer.InstallerFile.InstallerUrl, Path.Combine(currentInstallerDirectory, installer.Product.ProductId + // NOTE: by far not the best method to get a file extension, // consider the case where your absolute URL does not contain file extension Path.GetExtension(installer.InstallerFile.InstallerUrl.AbsolutePath)) ); } } private static void AddProductWithDependencies(Product product, List<Product> productsToInstall) { // avoid duplicates if (!productsToInstall.Contains(product)) { productsToInstall.Add(product); } // GetMissingDependencies will check whether any dependencies are not installed for the product // AND are not already added to the install list (productsToInstall) ICollection<Product> missingDependencies = product.GetMissingDependencies(productsToInstall); if (missingDependencies != null) { foreach (Product dependency in missingDependencies) { AddProductWithDependencies(dependency, productsToInstall); } } } } }
Acknowledgements
Thanks to Bilal Alam for the original idea.