David Shifflet's Snippets

Mindset + Skillset + Toolkit = Success




< Back to Index

Automating the Installs for Ancient (Legacy) Windows Applications

Friday, the end of the week. Your manager comes over to you at three in the afternoon with a simple request, "Can you bring up this old windows application on a new machine?"

"Sure", you reply then ask, "I was wondering if you have the install documentation for it?"

He grimaces and sternly states, "I have a word document from 1998 that is woefully out of date. By the way, we need this up by Monday."

Now your weekend is ruined or is it?

This is an example of how I would approach this problem. The goal here is to:

  • Automate the install so a new machine can be brought up quickly, or you could install it in the cloud.

Before Starting:

Review the documentation you have. Get the latest distribution to install the software. This is usually the "setup.exe" or "install-something.msi". Make sure it's the latest one!

First find a machine where the thing is installed and working. If this doesn't exist, the task will be harder. Go to that machine and see if the most recent install distribution (aka setup.exe) is around somewhere. Compare the one you think is the latest with what is deployed already. Basically confirm you have the right installs. If not, find it.

Look at the machine where it is working and note what programs are installed. "Add/Remove Programs" from the control panel will show you this. Chances are if it's an old machine there will be tons of stuff. You are going to want to identify the exact pre-requisites for the install in the next step.

Starting

Start documenting everything. Create a text file and take notes there. If you download something, paste the URL in your notes. If you run something write it down!

Create a new clean machine (instance). I typically bring up an EC2 machine on AWS (t2.micro windows datacenter). This takes two minutes for the instance to start, and then four minutes to get the Remote Desktop password to access it. You are going to delete this machine over and over again during this process. Reducing the time spent when you start over is important. If you go the physical machine route it may be slower.

Connect to the machine and copy your install over there. If you have pre-requisites copy them over. If you are downloading the pre-requisite software take note of the URLs. The downloads can be automated.

Start installing the software and trying to get things running by hand. You will want to do this from the command prompt. Take detailed notes about what you are doing and what is or is NOT working.

Hopefully it goes smooth and works!

When Things Go Sideways

During the install of the prerequisites or even when you try to run the application, things may fail. That information is going to most likely be in:

  • A message box in the UI.
  • The Console
  • The Install Log. These are sometimes in the working path, but usually in the temporary path.
  • The Event Viewer Application Log.

Take note of any DLLs or EXEs that are in the error text. Copy and paste the errors into your notes. Try to fix the errors. Google the errors.

Most likely one stumbling block will be the Microsoft Visual Studio C++ Run Times. Getting the right version of this is hard for some reason. Usually the documentation is wrong. Errors from this will complain about dlls typically named:

  • msvcrt.dll
  • msvcr100d.dll
  • msvc120.dll
  • mscrt90.dll

You can download these from Microsoft. The number in the DLL name is most likely the version of the C++ runtime. The version number in "msvcr120.dll" is "12.0" and this corresponds to "Visual C++ Redistributable Packages for Visual Studio 2013". The version numbers don't match the product names. This one is available at https://www.microsoft.com/en-us/download/details.aspx?id=40784. There is a list of version names and product names at https://en.wikipedia.org/wiki/Microsoft_Visual_Studio.

If you are dealing with a .NET application that uses some C++ code most likely your error will have SxS in it. This means side by side. Scroll through the error message to find out what is wrong, there will usually be a file listed like "msxml4r.dll". That dll is usually what is broken. It didn't get registered or is broke.

Important: It is tempting to just install all of the Visual Studio C++ Runtimes, every single one, in an attempt to get things working. Remember the goal here is find out the bare minimum of what to install. You can remove things and add things via Add/Remove programs but keep in mind ancient stuff doesn't always clean itself up during an uninstall. You may want to start over with a clean instance from time to time. When you go to automate it, you are going to want to do a final check that your install works on a clean instance. This is why shortening the length of the cycle from creating a clean instance and starting is important.

Common Last Chance Workarounds when things go sideways:

  • If an install fails because a DLL cannot be registered. Can you manually register the dll? "regsvr32 yourdll.dll"
  • Can you just copy DLLs to the application path and then it magically works?
  • Maybe Bill changed a common DLL in 1998 and didn't update the word document. Can you copy DLLs from where it works and copy and/or register those and get it working. (This works surprisingly often.)
  • Could it be permissions?
  • Could it be a syntax error on your part?

And Crazy Town

If all else fails get the install script out of the setup. https://stackoverflow.com/questions/8681252/programmatically-extract-contents-of-installshield-setup-exe.

Replicate by hand what it does and see where it fails. Try to correct it.

Eventually...

It's Working! Congratulations!

Great. Take a break. If you are using EC2 you could create a machine image and call it a day. What are the chances you won't need to repeat this on a new clean instance not from that image? Have you improved anything? The only documentation now is your notes and the original word document.

Automating the Installation

We will automate the installation and the current working documentation will be the script. That script will even test the installation to make sure it actually works.

We now know the bare minimum steps to install the ancient software and the process. The process usually is:

  • Download or copy the software
  • Install the software
  • Do strange things like tweaking the registry or updating text files
  • Clean Up the artifacts from the install
  • Test the Software

To automate this we will create:

  • An install.ps1 power shell script to install the software.
  • An install-check.ps1 power shell script to smoke test the software to make sure it works. The install script will call this at the end.

For this example we are going to write an install script for an application called FileConverter. This application takes a binary file and writes an XML file. To call it from the command line we use:

fileconverter.exe input.bin output.xml
During the manual install the following needs to be installed to get it working (Pre-Requisites):

  • Microsoft Visual C++ 2008 SP1 Redistributable Package (x86)
  • MSXML 4.0 Service Pack 3 (Microsoft XML Core Services)
  • Microsoft Access Database Engine 2010 Redistributable
  • A User DSN called "File-ConverterDSN" using the Microsoft Access Driver
  • Some Dart DLLs for dealing with compression. (https://www.dart.com/products/powertcp-zip-compression-for-net#overview)
  • NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)

The actual application installation is just a folder with an exe in it. We will create the install.ps1 file in that folder. We can then copy the folder, run install.ps1, and it will be ready to go.

I cannot share the fileconverter.exe with you. It's a hypothetical example.

Connect to the new clean instance. Create a folder to work from and let's create the install.ps1 file. Lines starting with "#" are comments.

$ErrorActionPreference = 'Stop'
# We are going to want some variables to store our paths and where the exes are.  It's easier to change these here than later on.

$system32path = [System.Environment]::SystemDirectory
$vcrFile = "$pwd\vcredist_x86_2008.exe"
$msXmlFile = "$pwd\msxml.msi"
$accessFile = "$pwd\AccessDatabaseEngine.exe"
$dsnName = "FileConverter_DSN"
$net22File = "$pwd\dotnet-hosting-2.2.6-win.exe"

# Always write something to the console for each step.  This will help people troubleshoot in the future.  If you are downloading something, use the product name this way if the URL changes someone in the future (YOU!) can find the new URL.  You could host your downloads somewhere else under your control.  Please review your software licenses to confirm this.

Write-Host "Downloading Microsoft Visual C++ 2008 SP1 Redistributable Package (x86)"
(New-Object System.Net.WebClient).DownloadFile("https://download.microsoft.com/download/1/1/1/1116b75a-9ec3-481a-a3c8-1777b5381140/vcredist_x86.exe", $vcrFile)

Write-Host "Downloading MSXML 4.0 Service Pack 3 (Microsoft XML Core Services)"
(New-Object System.Net.WebClient).DownloadFile("https://download.microsoft.com/download/A/2/D/A2D8587D-0027-4217-9DAD-38AFDB0A177E/msxml.msi", $msXmlFile)

Write-Host "Downloading Microsoft Access Database Engine 2010 Redistributable"
(New-Object System.Net.WebClient).DownloadFile("https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine.exe", $accessFile)

Write-Host "Downloading NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)"
(New-Object System.Net.WebClient).DownloadFile("https://download.visualstudio.microsoft.com/download/pr/a9bb6d52-5f3f-4f95-90c2-084c499e4e33/eba3019b555bb9327079a0b1142cc5b2/dotnet-hosting-2.2.6-win.exe", $net22File)

# Now that things are downloaded we can start to install them.  Notice the argument list, these are the parameters we are calling the file with.  Most installers will allow you to do quiet installs.  There is no format for these parameters.  Use the terms "quiet install" or "silent install" when searching the internet for how to do it with a particular product.

Write-Host "Installing Microsoft Visual C++ 2008 SP1 Redistributable Package (x86)"
Start-Process $vcrFile -ArgumentList "/qb" -Wait

Write-Host "Installing MSXML 4.0 Service Pack 3 (Microsoft XML Core Services)"
Start-Process "$system32path\msiexec.exe" -ArgumentList "/i", $msXmlFile, "/qn" -Wait

Write-Host "Installing Microsoft Access Database Engine 2010 Redistributable"
Start-Process $accessFile -ArgumentList "/quiet" -Wait

Write-Host "Installing NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)"
Start-Process $net22File -ArgumentList "/quiet" -Wait

# Access keeps access on the file for some reason so wait two seconds before deleting it.  As you automate things, you will notice strange problems and will need to work around them.  This is a good example.  

Start-Sleep -Seconds 2

# Checking if a process is still running is another thing that is often done.  An example would be when an installer starts a separate process quits and the other process continues to run.  The Start-Process Wait will return, but the install is not complete.  Here is an example of what to do:

# MCR setup may quit pretty fast and continues to install.  Let's watch the application that actually does the work
# The process is C:\MATLAB\mcr_setup\bin\win32
Write-Host "Waiting..."
while(get-process | ?{$_.path -eq $mcrSecondInstaller})
{
    Write-Host "Checking if running."
    Start-Sleep -seconds 15
}
#>

Write-Host "Cleaning Up"
Remove-Item -path $vcrFile
Remove-Item -path $msXmlFile
Remove-Item -path $accessFile
Remove-Item -path $net22File

Write-Host "Registering Dart DLLs"
Start-Process regsvr32.exe -ArgumentList "/s", "/i", "$pwd\dartzip.dll" -Wait
Start-Process regsvr32.exe -ArgumentList "/s", "/i", "$pwd\dartziplite.dll" -Wait
Start-Process regsvr32.exe -ArgumentList "/s", "/i", "$pwd\dartsock.dll" -Wait

# Why registering MS XML again after the install?  Because on Windows Server Core the MSXML 4 MSI install fails but this makes it work for our purposes.  Your install script may work on one platform and not on another.  This is a work around.  Basically copying the msxml4 dlls to the application path and registering makes the software work.

Write-Host "Registering msxml4.dll"
Start-Process regsvr32.exe -ArgumentList "/s", "/i", "$pwd\msxml4.dll" -Wait

# You can even setup ancient ODBC 32 User DSNs via Power Shell.

$dsn = Get-OdbcDsn -Name $dsnName -DsnType "User" -Platform "32-bit"
if($dsn) 
{
    Write-Host "Removing ODBC DSN: "$dsnName
    Remove-OdbcDsn -Name $dsnName  -DsnType "User" -Platform "32-bit"
}

Write-Host "Creating ODBC DSN: "$dsnName
Add-OdbcDsn -Name $dsnName -DriverName "Microsoft Access Driver `(`*.mdb`)" -DsnType "User" -Platform "32-bit" -SetPropertyValue "Dbq=$pwd\Database\fileconverter.mdb"
    
# Finally we call our install-check.ps1 script to see if it's working.
.$pwd\install-check.ps1
install.ps1 (Lines starting with # are comments)

Now we will create the install-check.ps1. If the software does not work we want to throw an exception that stops the script. Don't use a return value or something complicated. Just throw and stop. Most automatic deployment tools that might want to use these scripts are going to assume an exception means failure not a return code 100 vs return code 1024. Lines starting with "#" are comments.

$ErrorActionPreference = 'Stop'

# Need some variables for the things we will be working with.

$input = "$pwd\test.bin"
$actual = "$pwd\test.xml"
$expected = "$pwd\expected.xml"

Write-Host "Checking Health of File Converter (Convert $input to $actual)"
try 
{
    $item = Get-ChildItem -path $actual
    if ($item) 
    {
        Remove-Item -path $actual
    }
}
catch 
{
}

# We want to call the console application from the shell and capture the stdout to a variable.

# redirecting stdout to a variable instead of a file.  File is easy. No file is hard!
# https://stackoverflow.com/questions/8761888/capturing-standard-out-and-error-with-start-process
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = "$pwd\FileConverter.exe"
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = "`"$input`" `"$actual`""
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()

# compare our output to what is expected.  Power Shell has a lot of Cmdlets.

$diff = Compare-Object -ReferenceObject $(Get-Content $actual) -DifferenceObject $(Get-Content $expected)

if (($stdout.Trim() -eq "1") -And (!$diff)) 
{
    Write-Host "Healthy"
}
else 
{
    # throw the error not a return code or something complicated.
    throw "Not Healthy"
}
install-check.ps1 (Lines starting with # are comments)

After refining your scripts, it is going to work. You are almost there!

Finally

Create a new clean instance then remote desktop to it. Copy your folder over then fire up Power Shell and "./install.ps1"

Hopefully at the end you are greeted with "Healthy" and the deployment is now scripted.

You could bring up several of these things in the cloud. You could containerize the application using Docker and WindowsServerCore.

In a future blog post I will show you how to use docker and allow access to these applications over HTTP. Having the install automated will make that task easier.