Thursday, June 3, 2010

MSI Deployment of a Trusted Silverlight 4 Out-of-Browser Application

For our corporate network, we want to be able to deploy elevated trust Silverlight 4 applications to our users via an MSI deployment. The reason being that SL4's typical approach to installing an OOB application leaves a few things to be desired. Namely:

  • In order to do automatic updates, all XAPs must be signed. In some organisations this may not be an issue, but our department doesn't like the use of PKI.

  • In order to install an application, the user must first go to a web page where it is hosted. From then on they may run it from their Start menu. We'd like the app to be installed for them straight away.

  • Many of our users do hot-desking with their computers, and since OOB apps are installed on a per-user-per-workstation basis, these users would regularly be having to install (and update!) each application on a machine they haven't used before.



So what about sllauncher.exe?

Sllauncher.exe is great because it allows an OOB application to be installed without the user first having to go to a webpage. The shortcomings of Sllauncher are:

  • Running Sllauncher only installs the application for the current user, and only on that machine. Other users of the machine will not have the application installed. If the current user goes to another machine, they will have to then install or update the app again.

  • In order to do auto-updates, PKI must still be used.

  • Auto-updating of OOB apps is a little akward, since when an update is detected, the user is forced to exit and re-run the application before the updates take effect.



An MSI based solution

Since all our other applications are installed using MSIs and group policy, we wanted an MSI packaging option that fixed all these problems. In particular, we wanted the ability to:

  • Deliver the installation automatically via group policy when the user starts up their workstation.

  • Be able to deliver updates by either updating the XAP on the web, or even from a shared file location on the LAN

  • Not have the need for the user to exit the application before updates take effect.

  • Have the icon for the application automatically installed in their start menu.

  • Be able to take advangate of elevated trust without the need for PKI.



The solution was to create a wrapper around Sllauncher that could do this for us. It is installed in the Program Files directory of a client workstation, and is called instead of Sllauncher. It takes as a parameter the URI of the XAP for the application. When it is executed it does the following:

  • Checks to see if the OOB application is installed for the current user.

  • If the application is not installed, or if it is an older version than what is on the Intranet/LAN, it is updated.

  • Runs the application.



With this done, it is a relatively simple matter of creating a deployment project that includes this wrapper. Once installed, an app is deployed by giving the user a shortcut. Updates to the application are done by placing a new version of the XAP on the Intranet/LAN, and the wrapper will automatically update the client workstation when it is next run.

The code is shown below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Configuration;
using System.Net;

namespace SlLauncher2
{
class Program
{

// The path to the Sllauncher.exe
private static readonly string _sllauncher = string.Format("\"{0}\\Microsoft Silverlight\\sllauncher.exe\"", Environment.GetEnvironmentVariable("ProgramFiles"));

static void Main(string[] args)
{
// e.g. http://localhost/ClientBin/myapp.xap OR
// e.g. file://F:/Development/SL4Com/SL4Com/Bin/Debug/SL4Com.xap
string appUrl = args[0];
Uri appUri = new Uri(appUrl);

LaunchApplication(appUri);
}

/// <summary>
/// Does the work of installing or updating the application as necessary, and then executing it.
/// </summary>
/// <param name="appUri"></param>
private static void LaunchApplication(Uri appUri)
{
string xapFilename = appUri.Segments[appUri.Segments.Length - 1];

DateTime? installedXapLastModified; // The last modifed date of the currently installed XAP (if found)
DateTime? webXapLastModified =
GetLastModified(appUri);

// Determine the App ID of the installed version (null if not found)
string appId = GetAppId(appUri, out installedXapLastModified);

// If the xap was not found or the timestamps differ....
if (appId == null || webXapLastModified > installedXapLastModified)
{
// Install the app...
Console.WriteLine("Installing from web");
string fileName = DownloadXap(appUri);
InstallApp(appUri, fileName);
File.Delete(fileName);
}
else
{
Console.WriteLine("XAP was up to date");
}

// Run the app
OpenApp(GetAppId(appUri, out installedXapLastModified));
}

/// <summary>
/// Downloads the XAP to a temporary file.
/// </summary>
/// <param name="appUrl"></param>
/// <returns>The full path to the file.</returns>
private static string DownloadXap(Uri appUri)
{
WebRequest request = WebRequest.Create(appUri);
request.Method = "GET";

WebResponse response = request.GetResponse();
Stream receiveStream = response.GetResponseStream();

string fileName = Environment.GetEnvironmentVariable("TEMP") + @"\" + Guid.NewGuid().ToString();

FileStream output = new FileStream(fileName, FileMode.CreateNew);
BinaryWriter writer = new BinaryWriter(output);
BinaryReader reader = new BinaryReader(receiveStream);

byte[] buffer = reader.ReadBytes(1024);
while (buffer.Length > 0)
{
writer.Write(buffer);
buffer = reader.ReadBytes(1024);
}

reader.Close();
receiveStream.Close();
writer.Close();
output.Close();

return fileName;
}

/// <summary>
/// Determines the last modified date of the XAP at the specified URI.
/// </summary>
/// <param name="appUri"></param>
/// <returns></returns>
private static DateTime GetLastModified(Uri appUri)
{
WebRequest request = WebRequest.Create(appUri);

request.Method = "HEAD";

WebResponse response = request.GetResponse();

DateTime lastModified;

if (request.GetType() == typeof(HttpWebRequest))
{
lastModified = DateTime.Parse(response.Headers["Last-Modified"]);
}
else
{
lastModified = (new FileInfo(appUri.LocalPath)).LastWriteTime;
}

return lastModified;
}

/// <summary>
/// Launches the specified app ID
/// </summary>
/// <param name="appId"></param>
private static void OpenApp(string appId)
{
System.Diagnostics.Process.Start(_sllauncher, appId);
}

/// <summary>
/// Installs the specified OOB app
/// </summary>
/// <param name="appUrl"></param>
/// <param name="xapFilename"></param>
private static void InstallApp(Uri appUri, string xapFilename)
{
ProcessStartInfo startInfo = new ProcessStartInfo(_sllauncher, string.Format("/install:{0} /origin:{1} /shortcut:none /overwrite", xapFilename, appUri.OriginalString));
Console.WriteLine(_sllauncher + " " + startInfo.Arguments);

startInfo.RedirectStandardOutput = true;
startInfo.CreateNoWindow = true;
startInfo.UseShellExecute = false;

Process process = new Process();
process.StartInfo = startInfo;
process.Start();
string result = process.StandardOutput.ReadToEnd();
}

/// <summary>
/// Searches for the app ID of the given application
/// </summary>
/// <param name="appUrl"></param>
/// <param name="lastModified">The last modified date of the XAP, if found</param>
/// <returns>Null if not found</returns>
private static string GetAppId(Uri appUri, out DateTime? lastModified)
{
// This needs to be improved as it currently needs to be different on Windows XP/7.
string dataPath = Environment.GetEnvironmentVariable("APPDATA") + @"\..\Local Settings\Application Data\Microsoft\Silverlight\OutOfBrowser";
string appId = null;

lastModified = null;

// Search each metadata file for the XAP
foreach (string directory in Directory.GetDirectories(dataPath))
{
string file = directory + @"\metadata";
if (File.Exists(file))
{
using (StreamReader reader = new StreamReader(file, Encoding.Unicode))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();

if (line.Contains("OriginalSourceUri=" + appUri.OriginalString))
{
FileInfo fileInfo = new FileInfo(file);

appId = (new FileInfo(directory)).Name;
lastModified = fileInfo.LastWriteTime;
break;
}
}
}
}
}

return appId;
}
}
}

Silverlight 4 Deployment Guide

Documentation from Microsoft:
Silverlight 4 Deployment Guide
Group Policy Settings

Group policy settings allow control of:

  • Digital Rights Management — enable or disable playback of DRM enabled content

  • Silverlight Automatic Update Mechanism — disable the automatic update mechanism which is separate from Microsoft Update

  • Silverlight Trusted Applications — allows users to install out-of-browser applications via the Install dialog

  • WebCam and Microphone — allows webpages and applications to access the microphone and webcam

  • UDP Multicast Networking — allows webpages and applications to do UDP multicast networking