Deploying Adobe Shockwave Player with System Center Configuration Manager

Welcome to part two of a five-part series on deploying runtimes and reader from Adobe with an app store-like experience using Microsoft System Center Configuration Manager. Today’s episode: Shockwave Player! As I prepared to write, I asked myself, “Does anyone use Shockwave anymore?” It turns out that the answer is pretty much a “no”, but I’m going to stick with my plan to cover Shockwave, Flash, AIR, and Acrobat Reader (in that order) for completeness. Also, Shockwave is the only one of the four that is actually packaged appropriately by the vendor for an acceptable user experience, so it makes sense to cover it first so we can focus on the plumbing of building the application package. Each of these programs receives frequent updates, and it can be quite a task to build new application packages every few weeks. As I have mentioned previously, there are vendors that will take care of this for you, and I encourage you to use one of them if you can afford it and if it makes sense in your organization. My approach here will be to show a do-it-yourself method for automating app package creation for these frequent updates using Windows PowerShell.

Prerequisites

To follow along, you will need to have distribution rights from Adobe, PowerShell 5.0, and the Configuration Manager cmdlets. See my previous blog post for details. You may not technically need PowerShell 5.0, but that’s what I have. I’m not going to make any effort to ensure that what I’m doing works on previous versions, so to make sure everything below works for you, I suggest upgrading to version 5.0 or higher. You can check your version of PowerShell from a PowerShell prompt by displaying $PSVersionTable.PSVersion. Here’s the command and its output from my Windows 10 v1511 computer with PowerShell 5.0 installed:

PS C:\> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      0      10586  494

Using PowerShell for Packaging: First Iteration

Let’s begin by outlining the plan in comments. We’ll start with a simple, single-use script and then refine it later.

# Download the Shockwave installer from Adobe

# Extract the version information from the installer

# Save the installer to the appropriate location

# Extract the program icon

# Create the application object in Configuration Manager 

Downloading

There are several ways to download a file from the web with PowerShell. I’m going to choose the simplest one (which may not be the “best” one, depending on your criteria): a built-in PowerShell cmdlet called Invoke-WebRequest. Start by clicking the Shockwave link you received from Adobe. In the Shockwave Player Distribution Downloads table, right-click the Download MSI Installer link, and choose Copy shortcut or its equivalent to copy it to the clipboard. Paste the URL into the PowerShell script and assign it to a variable. This will be important later. Basically, we’re going to use Invoke-WebRequest to download the file to a temporary folder. Unfortunately, things are never as easy as they sound. The download link provided by Adobe does not include a filename, but Invoke-WebRequest needs one in order to save the file. It is important (to me, at least) to use the real filename of the file rather than something arbitrary. Also, the link provided by Adobe redirects elsewhere, which complicates matters. Here is the somewhat scary-looking code, and an explanation follows.

# Download the Shockwave installer from Adobe

#$Uri = "[Shockwave-download-URI]"

# We don't know the installer's filename, so generate a temporary one until the file is downloaded 
# and we can get the filename from the WebResponseObject.
$tempOutFilename = [System.IO.Path]::GetRandomFileName()
$tempOutFilePath = "$env:TEMP\$tempOutFilename"

# Download the installer and save it to a temporary location and filename.
# The PassThru switch puts a WebResponseObject in the pipeline in addition to writing the file.
$response = Invoke-WebRequest -Uri $Uri -OutFile "$tempOutFilePath" -PassThru

# Get the filename from the WebResponseObject.
$outFilename = [System.IO.Path]::GetFileName($response.BaseResponse.ResponseUri.AbsoluteUri)

# If the target file already exists, delete it.
# If the file does not exist, Remove-Item returns an error, so ignore errors.
$outFilePath = "$env:TEMP\$outFilename"
Remove-Item -Path "$outFilePath" -Force -ErrorAction Ignore

# Rename the temporary file with its proper name.
Rename-Item -Path "$tempOutFilePath" -NewName $outFilename

# Remove zone identifier.
Unblock-File -Path "$outFilePath"

Obviously, replace the URI placeholder text with the appropriate download link from Adobe. Since that link does not include the filename, we generate one with a call into the .NET Framework’s System.IO.Path.GetRandomFilename() function. Next, we use Invoke-WebRequest to download the file to a temporary folder. The Invoke-WebRequest cmdlet puts its result on the pipeline as a WebResponseObject unless you pass the OutFile parameter, in which case it puts nothing on the pipeline. The PassThru switch overrides this behavior and puts the response object on the pipeline in addition to writing the output file. This object, which is captured by the response variable, contains the actual URI used to obtain the downloaded file after any redirects were followed. The System.IO.Path.GetFileName() function extracts just the filename from this URI. We delete any existing file with the same name in the temporary folder and then rename the downloaded file with the proper name. Finally, we remove the zone identifier from the downloaded file with Unblock-File in order to prevent security prompts when attempting to install the program later. For more information on this, see About URL Security Zones on TechNet. Scott Hanselman also has a great discussion of the zone identifier alternate data stream from 2007, but he doesn’t mention the PowerShell cmdlet we will use, so maybe it did not yet exist back then.

Extracting Version Information

The download link and the file we downloaded does not contain any version information in the name, but we will need the version in order to make the Configuration Manager application. I have not found any built-in PowerShell cmdlets to extract version information from a Windows Installer (MSI) file, but fortunately, we have the Internet to help us!

I found a number of scripts in a number of places, but I chose to use the one from Nickolaj Andersen at scconfigmgr.com because it was the simplest that suited my needs. Visit that link, follow the instructions there, and then come back here to continue. When you’re finished, you’ll have a Get-MSIFileInformation.ps1 file in the same folder as the script we are writing here.

Since we’re leveraging an outside script for most of the work here, our own version extraction code is quite short.

# Extract the version information from the installer

$newMsiVersion = .\Get-MSIFileInformation.ps1 -Path "$outFilePath" -Property ProductVersion

#Get-MSIFileInformation returns an array for the ProductVersion property. Convert it to System.Version via a string.
$newMsiVersion = [System.Version]([System.String]$newMsiVersion).Trim()

Unfortunately, the output of the Get-MSIFileInformation function is not of the type that we need, and it cannot be converted directly to a System.Version. To overcome this, we cast it to a string, remove the leading spaces with Trim(), and then cast the string to System.Version.

Saving the Installer to the Proper Location

Now that we’ve downloaded the installer and extracted the version number, it’s time to move it to its final location. This location should be wherever you normally save application installation files for use by Configuration Manager. We’ll supply the root folder, the manufacturer name, and the product name, and then the script will concatenate it all together to make a folder structure of “\\server\share\manufacturer\product ww.xx.yy.zz” for the installer.

# Save the installer to the appropriate location
$Destination = "\\server\share"
$Manufacturer = "Adobe"
$Product = "Shockwave Player"
$productNameAndVersion = "$Product $newMsiVersion"

$destFolder = "$Destination\$Manufacturer\$productNameAndVersion"
New-Item -Path $destFolder -Force -ItemType Directory
Move-Item -Path "$outFilePath" -Destination "$destFolder\$outFilename" -Force

Alert readers will have noticed my inconsistent capitalization of variable names. I did that on purpose because I am going to convert some of the variables to parameters later. Future parameters are in Pascal case; regular variables are in camel case.

Extracting the Program Icon

If you supply an icon file when creating a Configuration Manager application, Software Center will use that icon rather than the generic program icon. This looks more professional and makes Software Center seem more legitimate to the user as a real app store.

The cmdlet we will use later to create the ConfigMgr application accepts only image file formats; it does not accept EXEs, DLLs, or ICOs. How, then, does one extract the needed icons for use with this cmdlet? I would like to be able to extract the highest-quality icon from the EXE setup file, but unfortunately, there does not appear to be a built-in facility for doing so in .NET. I found some native code examples online, so I may revisit this in the future. In the meantime, we’ll make use of the System.Drawing.Icon class.

It takes a little bit of work to get the program icon out of a Windows Installer file. The methodology we will use is:

  1. Perform an Administrative Installation of the MSI in order to extract all of its files.
  2. Extract the desired icon from the main program file (EXE) and save it somewhere for use later.
  3. Delete the Administrative Installation files and folders.
# Extract the program icon

$administrativeInstallFolder = New-Item -Path "$env:TEMP" -Name ([System.IO.Path]::GetRandomFileName()) -ItemType Directory
$administrativeInstallArguments = @("/a", "`"$destPath`"", "/passive", "/norestart", "TARGETDIR=`"$administrativeInstallFolder`"")

Start-Process -FilePath "$env:SystemRoot\System32\msiexec.exe" -ArgumentList $administrativeInstallArguments -Wait

$pathToExe = "$administrativeInstallFolder\System32\Adobe\Shockwave 12\SwInit.exe"
$iconFolder = New-Item -Path "$env:TEMP" -Name ([System.IO.Path]::GetRandomFileName()) -ItemType Directory

$exeIcon = [System.Drawing.Icon][System.Drawing.Icon]::ExtractAssociatedIcon($pathToExe)
$extractedIconFilename = [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName())+".png"    #Remove generated filename extensions and add PNG.
$exeIcon.ToBitmap().Save("$env:TEMP\$extractedIconFilename", [System.Drawing.Imaging.ImageFormat]::Png)
$exeIcon.Dispose()

Remove-Item -Path $administrativeInstallFolder -Recurse -Force

This section of code leaves us with a PNG file of Shockwave’s main program icon saved in the temporary folder with the name stored in the $extractedIconFilename variable.

Creating the Configuration Manager Application

With all of the requisite pieces prepared, it is time to create the application object. We’ll use splatting to improve the readability of the code. The ConfigurationManager PowerShell module (one of the prerequisites) contains the needed cmdlets.

Application Object

# Create the application object in Configuration Manager

Import-Module ConfigurationManager

# For clarity, prepare all arguments for the Configuration Manager cmdlets in advance via splatting.
$description = "Over 450 million Internet-enabled desktops have Adobe Shockwave Player installed. These users have access to some of the best content the Web has to offer - including dazzling 3D games and entertainment, interactive product demonstrations, and online learning applications. Shockwave Player displays Web content that has been created using Adobe Director."
$appParams = @{
                Name = $productNameAndVersion;
                AutoInstall = $true;            # General Information tab: "Allow this application to be installed from the Install Application task sequence action without being deployed"
                Description = $description;
                IconLocationFile = "$env:TEMP\$extractedIconFilename"
                IsFeatured = $false;            # "Application Catalog tab: Display this as a featured app and highlight it in the company portal"
                LocalizedApplicationDescription = $description;
                LocalizedApplicationName = $appName;
                Owner = $env:USERNAME;          # Replace this with something more descriptive if desired, such as "FirstName LastName (email@example.com)".
                Publisher = $Manufacturer;
                ReleaseDate = (Get-Date -Hour 0 -Minute 0 -Second 0 -Millisecond 0);
                SoftwareVersion = $newMsiVersion;
                SupportContact = $env:USERNAME
              }

$ConfigurationManagerApplicationLocation = "CM1:\Applications\Free" #Replace with script parameter later
Set-Location -Path $ConfigurationManagerApplicationLocation

$cmApp = New-CMApplication @appParams
Move-CMObject -FolderPath $ConfigurationManagerApplicationLocation -InputObject $cmApp

Remove-Item -Path "$env:TEMP\$extractedIconFilename" -Force

All the parameters that we care about for New-CMApplication are included in the $appParams variable. The ConfigurationManager module’s cmdlets require use of a special PSDrive in order to function, so you should substitute your site name for “CM1” and your preferred location for the application object for “Applications\Free” in the $ConfigurationManagerApplicationLocation variable. Unfortunately, regardless of the current directory, the New-CMApplication cmdlet creates application objects in the Applications folder only, so we must use the Move-CMObject cmdlet to put it where we want it. Finally, we use Remote-Item to delete the extracted icon file.

All of this code creates an application object and puts it in the desired location, but so far, it doesn’t actually install any software. To do that, we must add a deployment type.

Deployment Type

We’ll use splatting again to simplify the code; all of the parameters that we care about for the deployment-type-creation cmdlet are included in $dtParams.

$installCommand = "msiexec /package ""$outFilename"" /quiet /norestart"

$dtParams = @{
                InputObject = $cmApp;
                ContentLocation = $destPath;
                InstallCommand = $installCommand;
                DeploymentTypeName = "$newMsiProductName - Windows Installer (*.msi file)"; # Match what ConfigMgr Console would name it by default
                LogonRequirementType = "WhetherOrNotUserLoggedOn";
                UserInteractionMode = "Hidden";
                InstallationBehaviorType = "InstallForSystem"
             }
Add-CMMsiDeploymentType @dtParams

The installation command is a standard Windows Installer command line that specifies no user interface and blocks any requested computer restart requests. MsiExec.exe will, however, return a special value (1641 or 3010) if a restart is needed, and since we didn’t override the return values, the defaults in Configuration Manager will honor this and cause Software Center to notify the user. (In a task sequence, the Configuration Manager client will restart the computer, if needed, and then continue with the next task sequence item.)

We specify the application object created earlier as the InputObject so that the cmdlet knows which one should get this new deployment type. We specify the full path and filename of the MSI file to the ContentLocation field so that the cmdlet knows which MSI file to read, but content location in the resulting deployment type that we create only contains the path. Because we are supplying an MSI file to an MSI-specific cmdlet, the cmdlet figures out the uninstallation command and detection logic on its own, so we need not specify these items. The values for the LogonRequirementType, UserInteractionMode, and InstallationBehaviorType fields are standard for non-interactive installations that are intended for use from Software Center or in a task sequence.

Coming Up

That’s it for now. You can run this script at any time to get the latest Shockwave Player downloaded and imported into Configuration Manager.

Now, this script leaves room for improvement. Here are some to-do items for the future:

  • Refactor the code into several functions.
  • Parameterize to add flexibility.
  • Generalize the code to make it work for other, similarly-distributed applications.
  • Rewrite the icon-extraction code to get a higher-resolution icon, if available.

I’m going to tackle these items in future posts in this series. In addition, once this blog series is finished, I plan to post the final code on GitHub to facilitate easier usage than copying and pasting from numerous code snippets.

Next time, we’ll take a look at Adobe Flash Player.