Check out my new blog at https://shibumiware.blogspot.com

Thursday, June 08, 2017

Automatic Updates - Tips & Tricks

Product Update
(I Received feedback about "Moving On" and decided to
just syndicate my posts to both blogs" 
 

Introduction


Almost every modern Windows and phone app has some method to update itself.  This increases productivity, security, and an automatic update channel, allows the vendor to put more features into the user's hands faster. You might build applications for fun, donations, or maybe it's your full-time job, and a mechanism for automatically updating can be a fantastic time-saver.

There are challenges.  Many corporate IT policies prevent users from installing applications or applying updates.  This does make sense in many cases ranging from maintenance issues to avoiding increasing the threat surface area of information systems.  There are ways around this obstacle such as not attempting to install applications the traditional way through a setup prog, am.  Usually these setup programs will install into Programs or Programs (x86), make registry entries in HKEY_CURRENT_MACHINE or any number of actions that require administrative or elevated rights.

If all the application does are things a regular user can do, its hard to something you aren't allowed to do.  As Raymond Chen refers to It(s) rather involved being on the other side of this airtight hatchway

On the other hand, unless the IT group has the user desktop locked down super tight, you can copy files into directories "owned" by the users and, if necessary, make changes to HKEY_CURRENT_USER key (depending on what and where) if your app requires registry access.  I am not necessarily advocating this, but that's the way my hobby projects ship.

The process of updating is seemingly trivial. House a catalog of updates with a manifest somewhere on the internet. Every time the app starts or, on the user's behest, go grab the manifest, and compare the catalog version with the currently installed version. If the catalog's version is higher "download and apply the update".

Notice the quotations, which could signal sarcasm from the author.

High-Level Update Flow - Reference for Further Discussion

Product High Level
The diagram on the right points to items we will cover later in the post.  I put it out there now because you can see some of the nuts and bolts, and you can noodle on what embedded assemblies and applications are, and why we might want them.

A couple things to just forget about trying to do with app updates.  Forget about trying to secure your catalog.  You certainly can't do it by obscuring its location.  Anyone with Fiddler or WireShark will figure out where your catalog is in about 8 seconds.  Second, don't try to encrypt your catalog because the key distribution is impossibly insecure unless you use asymmetric algorithms, but who wants to ship their public key with their app and then establish a handshake that allows the user to decrypt the downloaded catalog?  It just needs to be a web page or if it's more convenient, open up a public blob store in Azure, which is what I am going to do at some point.

My catalog is actually just a Blogger page, with a text area that contains a simple XML document.  A user is free to go look at the catalog if they want.

Set a baseline version of .NET for your update framework, not necessarily the version that your apps are written in so that you can at least get the user started.  As an example, my installer is written in .NET 2.0 but most of my apps require .NET 4.0.  Most computers will have .NET 2.0 installed already, but there is no guarantee.  Actually, I think you would have to go back to Windows XP to find .NET 1.1 running and I don't focus on that class of users anyway, but you should provide a decent user experience of the required version of the framework is not present.

The update system has three layers, as you can see in the diagram below.  The first is a bootstrap executable written in C/C++, which knows how to find if .NET 2.0 is installed.  If it is installed, the bootstrap kicks off the System Check executable, which is fully contained--meaning that all assemblies that are not part of the .NET 2.0 Base Class Libraries (BCL) are embedded resources and are loaded at run-time. If the bootstrap detects that .NET 2.0 is NOT installed, it will take the user to a Microsoft-provided web page with instructions on how to install the framework.  Note that it also indicates that the user must have administrative privileges to do the installation.  More than likely, the user will end up installing a version of .NET must more current than 2.0 if they get that far.

I will discuss how to load create and load embedded assembly resources later in the post.

Layers

Boot Strap

Welcome back to C/C++ for those who have been away a while.  As you might imagine, aside from great tooling and the usual Microsoft-specific extensions to the languages, not much has changed given that the current state of the art in straight C is C11 and for C++ there is the C++14 standard, and the forthcoming C++17 standard, of which Microsoft has various levels of conformance.

The bootstrap executable is incredibly simple.  It only does three things: checks to see if .NET 2.0 is installed; if not, present a web page explaining how to install it; and finally, if .NET 2.0 is installed, spawns the System Check application.

I am not sure how well my HTML-Copy-Code Visual Studio extension will work for C, but let's give it a whirl, shall we?  Okay, I tried that.  It does NOT understand C/C++.  I found one online that did the trick nicely!  The palette is different but beggars can't be choosers.

You are welcome to skip over the listing.

BootStrapper.exe Listing

include "stdafx.h"

// Global variables
HINSTANCE _Instance;

// Constants
const int ERROR_UNEXPECTED = 3;
const int ERROR_FRAMEWORK_NOT_INSTALLED = 4;
const int ERROR_INVALID_INSTALLER = 5;
const int ERROR_INSTALLER_LAUNCH_FAILED = 6;

const int MAX_BUFFER_SIZE = 2000;

const wchar_t CONSTANT_FRAMEWORK_REGISTRY_KEY[] = L"SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v2.0.50727";
const wchar_t CONSTANT_FRAMEWORK_LINK[] = L"https://msdn.microsoft.com/en-us/library/aa480243.aspx#ndp2_ref_topic2";

// Defaults
wchar_t CONSTANT_DEFAULT_PATCHER[] = L"SW.Apps.SystemCheck.exe";

// Prototypes
void LoadFrameworkGuidance();
bool IsFrameworkInstalled();
bool GetInstallerPath(LPTSTR commandLine, LPWSTR installerPath);
bool IsValidInstaller(LPWSTR filePath);
int LaunchInstaller(LPWSTR installerFile, LPTSTR lpCmdLine);
int ShowMessageBox(UINT stringId, UINT type);

//  Entry point
int APIENTRY _tWinMain(__in HINSTANCE hInstance,
                       __in_opt HINSTANCE hPrevInstance,
                       __in LPTSTR lpCmdLine,
                       __in int nCmdShow)
{
    _Instance = hInstance;

    int returnValue = ERROR_SUCCESS;

    // Is the 2.0 Framework installed?
    if (!IsFrameworkInstalled())
    {
        // No, load guidance
        LoadFrameworkGuidance();

        returnValue = ERROR_FRAMEWORK_NOT_INSTALLED;
    }

    if (returnValue == ERROR_SUCCESS)
    {
        // Yes, load the exe off the command line or run the default exe name defined above
        wchar_t installerFile[MAX_PATH * sizeof(wchar_t)];

        if (!GetInstallerPath(CONSTANT_DEFAULT_PATCHER, (LPWSTR)&installerFile) || !IsValidInstaller(installerFile))
        {
            returnValue = ERROR_INVALID_INSTALLER;
        }

        if (returnValue == ERROR_SUCCESS)
        {
            // Got a good file path, launch it
            returnValue = LaunchInstaller(installerFile, lpCmdLine);
        }
    }

    switch (returnValue)
    {
    case ERROR_FRAMEWORK_NOT_INSTALLED:
        // Bail
        break;
    case ERROR_INSTALLER_LAUNCH_FAILED:
        // Error message
        ShowMessageBox(IDS_INSTALLER_LAUNCH_FAILURE, MB_OK | MB_ICONERROR);
        break;
    case ERROR_INVALID_INSTALLER:
        // Error message
        ShowMessageBox(IDS_INVALID_INSTALLER, MB_OK | MB_ICONERROR);
        break;
    default:
        break;
    }

    return returnValue; // Just in case a caller wants to know what the result was
}

void LoadFrameworkGuidance()
{
    if (ShowMessageBox(IDS_PROMPT_LEARN_ABOUT_FRAMEWORK, MB_YESNO | MB_ICONQUESTION) == IDNO)
    {
        return;
    }

    ShellExecute(NULL, 0, (LPCWSTR)&CONSTANT_FRAMEWORK_LINK, 0, 0, SW_MAXIMIZE);
}

bool IsFrameworkInstalled()
{
    HKEY keyHandle;
    LSTATUS returnValue = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
        CONSTANT_FRAMEWORK_REGISTRY_KEY,
        0,
        KEY_READ,
        &keyHandle);

    if (keyHandle != NULL)
    {
        RegCloseKey(keyHandle);
        return true;
    }

    return false;
}

bool GetInstallerPath(LPTSTR commandLine, LPWSTR installerPath)
{
    DWORD len = GetModuleFileName(NULL, (LPWCH)installerPath, MAX_PATH * sizeof(wchar_t));

    if (len == 0)
    {
        return false;
    }

    while (len--)
    {
        if (installerPath[len] == '\\')
        {
            installerPath[len + 1] = -'\0';
            break;
        }
    }

    if (len > 0)
    {
        int bufferSize = MAX_PATH * sizeof(wchar_t);
        wchar_t buffer[MAX_PATH * sizeof(wchar_t)];

        if (wcscpy_s(buffer, bufferSize, installerPath) == ERROR_SUCCESS)
        {
            if (wcscat_s(buffer, bufferSize, commandLine) == ERROR_SUCCESS)
            {
                return wcscpy_s(installerPath, bufferSize, buffer) == ERROR_SUCCESS;
            }
        }
    }

    return false;
}

bool IsValidInstaller(LPWSTR filePath)
{
    DWORD attributes = GetFileAttributes(filePath);

    return attributes != 0xFFFFFFFF;
}

int LaunchInstaller(LPWSTR installerFile, LPTSTR lpCmdLine)
{
    STARTUPINFO startupInfo;
    PROCESS_INFORMATION process;

    ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
    ZeroMemory(&process, sizeof(PROCESS_INFORMATION));

    if (CreateProcess(installerFile, lpCmdLine, NULL, NULL, true, 0, NULL, NULL, &startupInfo, &process))
    {
        if (WaitForSingleObject(process.hProcess, INFINITE) == ERROR_SUCCESS)
        {
            return ERROR_SUCCESS;
        }

        CloseHandle(process.hThread);
    }

    return ERROR_INSTALLER_LAUNCH_FAILED;
}

int ShowMessageBox(UINT stringId, UINT type)
{
    wchar_t prompt[MAX_BUFFER_SIZE];
    wchar_t caption[MAX_BUFFER_SIZE];

    int len = LoadString(_Instance, stringId, (LPWSTR)&prompt, MAX_BUFFER_SIZE);

    if (len == 0)
    {
        return ERROR_UNEXPECTED;
    }

    len = LoadString(_Instance, IDS_APP_TITLE, (LPWSTR)&caption, MAX_BUFFER_SIZE);

    if (len == 0)
    {
        return ERROR_UNEXPECTED;
    }

    return MessageBox(NULL, (LPCWSTR)&prompt, (LPCWSTR)&caption, type); 

}       
Boot Strapper remains almost constant.  Aside from changing the registry key path and the link to the installation web page, this code hasn't changed in 10 years or so (except for the inclusion of the annotations on _tWinMain like "__in" and "__in_opt", which are part of Microsoft's SAL ( Source code Annotation Language ).

System Check

System Check performs a number of much more involved operations than Boot Strapper.  First, it is responsible for retrieving the catalog data from the web, building a set of objects from the catalog to be used later. Secondly, it scans the local machine for installed apps, which uses the same objects as the online catalog source but with additional information about the app's local context. Third, it presents a "gallery" of updates, which is a user interface by which the user can pick and choose which updates to apply.  Furthermore, the gallery shows all available products, so if they find something new, the can install the app immediately.  System Check contains all of the code required to download an update or install an app.

The very first thing it does, however, is check to see if there is a new version of itself--the update the updater problem.

Challenge I: Updating the Updater

System Check is just another app of sorts, right?  What makes updating it different than updating any other app?  The main difference is that it is running already.  If you are updating another application, and it happens to be running, simply prompt the user to save their work and close the app.  System Check waits for this to occur or the user chooses to abort the update, and then System Check carries on or not.

But, with System Check you can hardly ask it to close itself and somehow magically update itself.  That takes work.

System Check Update Flow
It's easier to view if you zoom your browser to about 130%.   There are four lanes: System Check, Data (Web & Local), File Replacement, and System Check once again.  The second System Check lane represents the latest running version.  Let's summarize:

  1. System Check v1.0.0.0 starts and downloads the catalog XML.  Regardless of whether a self-update occurs, this is the first action taken by System Check
  2. System Check compares its version with the version in the catalog.  If it is the same, the flow drops down to the 4th lane and regular update actions continue
  3. If the catalog version is higher, System Check downloads the newest version from the web
  4. System Check unpacks the zip file into a temporary folder and deletes the downloaded zip file
  5. Embedded in System Check is an executable called SW.Apps.FileReplace.exe (File Replace) which is compiled into System Check as an embedded binary file.  System Check unpacks File Replace into the temporary folder created above
  6. Not on the flow chart, but important, is System Check generates an "install.info" file in the temporary folder.  This file contains information about how File Replace behaves and what actions it performs. Specifically, it contains the path to the executable that called it, the full path to the setup it will execute, the flags contained in the catalog XML (flags drive file operations such as specifying a wildcard used to determine what types of files should be deleted so if there are user data files, those can be excluded), and the installer type.  For System Check, the type is always a ZIP archive because it contains just one file
  7. System Check executes File Replace with System Check's process ID and the location of the temporary folder on the command line
  8. File Replace starts, waits for System Check to close (the reason the process ID is passed on the command line), and then looks for "install.info" in the folder indicated on the command line
  9. File Replace performs the actions required to replace System Check (delete files and optionally subdirectories and files, copies the new version into the original folder and cleans up as well as it can
  10. Keep in mind that now we have another problem in that File Replace plus the temporary directory and install.info are on the drive and we don't want to leave these files laying around
  11. File Replace executes System Check, passings its process ID and location on the command line, and begins to close down
  12. A piece of code common to all my apps, not just System Check, is PerformCallerActions.  When System Check is started up, it passes the command line to PerformCallerActions, which knows how to do various things
  13. PerformCallerActions waits for File Replace to quit and then deletes all of the temporary files on the disk related to the update process
  14. System Check v1.0.0.1 continues with its normal update actions
  15. <The End/>

Challenge II: Self-Contained Executables (No External File References)

This topic is less of a challenge and more about knowing how to do it.  The idea is simple.  Create an executable that takes external references outside of the Base Class Libraries (BCL-those libraries that ship with .NET) and fashion a mechanism whereby your executable totes its references around with it as embedded resources instead of external files.  Why would you want to do this besides "its cool"?  When it comes to setup, in particular, I want my setup and update executables to rely on nothing on the machine except for the BCL.  I don't want to ship 5 files so I can setup an app that has 7 files.

AppDomain.CurrentDomain.AssemblyResolve to the rescue!  This event is fired each time the App Domain attempts to load an assembly using the assembly load sequence and fails.  Keep in mind it is important you create your reference with Specific Version = True otherwise the load sequence may find an assembly of another version, load it, and potentially cause all sorts of problems.  So, after .NET performs its duties attempting to locate the assembly by its fully qualified name (i.e. Ionic.Zip, Version=1.9.1.5, Culture=neutral, PublicKeyToken=edbe51ad942a3f5c) and fails, it fires the AssemblyResolve event.

The handler will always look something like this:

private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    if (args.Name.Equals(InstallerConstants.ZIP))
    {
        return Assembly.Load(Resources.Zip);
    }
 
    return null; (or do something else like throw up an error message but assuming because you implemented the handler, you intend to do something else :-)
}
internal const string ZIP = @"Ionic.Zip, Version=1.9.1.5, Culture=neutral, PublicKeyToken=edbe51ad942a3f5c";        
I always use a constant instead of a setting or something that can be changed in this sort of situation because if I change the version, it will demand recompilation to be successful.  Testing the assembly resolve handler will uncover the issue easier if you make this a const.

The purpose of the event is for you to take action when the assembly load sequence fails.  Sometimes the action is to throw your hands up and tell that user it is missing a required file.

Embedded File
However, you can be creative with it.  The inbound "args" includes the fully qualified name of the assembly it was unable to locate and its return value is an Assembly.  Assembly.Load has many overloads.  Of interest to us is the overload that accepts a byte array (byte[]) that contains the full bytes of a file image of the assembly.   Simply add a resource (.resx) file to your project and add the assembly as a file.  As you can see in the screenshot, it is stored as a byte array.

To ensure you are using the event and not the normal assembly binding mechanism, set Copy Local = False in the reference. The file will no longer be in the assembly's load sequence (unless it finds it somewhere else--beware of the GAC!) and the event will fire. If the embedded file is not a DLL provided through NuGet or some other mechanism but an executable you created, the easiest thing to do is add the executable to your Resources folder as a link to the build output of said executable. The compilation process will take care of grabbing it from it build location and embedding it in your executable.

As an interesting side note: System Check uses this mechanism for Ionic's ZIP library, HTML Agility Pack, and SW.Apps.FileReplacement.  File Replacement also has an embedded reference to Ionic ZIP, so we are embedding an executable with an embedded reference.  Not bad for a few hours work.

Product Update

Building the Update System

Up until now, we have talked about under-the-covers tips and tricks.  Let's take a look at a real-world example (mine).  Note that all of the code up to this point has been hanging around in my stack in various forms and levels of maturity.  Maybe 5-7 years ago, I built an update system called QuickPatch (and here).  It was too complex, trying to do too many things.  These days I use off-the-shelve installers like WIX instead of rolling my own (with one exception, which I will discuss in a follow-on post).  The only thing "invented" here is the approach to checking for new versions, kicking off the setup applications, and creating a user interface that I am satisfied with.  And, of course, updating the updater, which was fun.

Here is what mine in its earlier state looks like (with test data).



Gallery




I may follow up with any lesson's learned.


Hope you enjoyed this!






Colby-Tait

No comments :

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.