Synchronise App-V 5 Client with Publishing Server

I’ve recently set up a couple of virtual machines to start sequencing and testing App-V 5 packages.  When setting up the client, rather than configure it for local testing I wanted to synchronise it with the App-V 5 publishing service.  We can do this (at the most basic level) using a PowerShell one-liner:

Add-AppvPublishingServer -name "<NAMEOFSERVER>" -Url "<URL OF Publishing Service>" | Sync-AppvPublishingServer

When you run this from an elevated PowerShell prompt, you may see one or more of the following errors:

Sync-AppvPublishingServer – Application Virtualization Service failed to complete requested operation.

HttpRequest sendRequest failed.

Getting server publishing data failed.

This is probably because the URL you specified is incorrect.  To find the correct URL, navigate the registry on your App-V 5 publishing server to the following location:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\AppV\Server\PublishingService

Grab the value of PUBLISHING_MGT_SERVER.  Replace ‘localhost’ with the fully qualified domain name (FQDN) of the server itself.  And replace the port with the value in PUBLISHING_WEBSITE_PORT.  Assuming that:

PUBLISHING_MGT_SERVER=http://localhost:8000/

PUBLISHING_WEBSITE_PORT=8001

and the FQDN of the server is appv5.alkane.uk

Your PowerShell one-liner would be:

Add-AppvPublishingServer -name "ALKANEAPPV5" -Url "http://appv5.alkane.uk:8001/" | Sync-AppvPublishingServer

To verify this, if you put the URL into a browser session you should see an XML view of published packages for the authenticated user.

App-V 5 and Killing Child Processes

I recently packaged an application that, upon launch, started a process called EntDistributorService.exe (the parent process).

EntDistributorService.exe spawned a child process called hvced.exe (I knew hvced.exe was a child process by running the wmic command found here.), which remained running when the launched application was closed.

When a process contained within the App-V package remains open, it presents issues when we need to unpublish it – the App-V package can’t be unpublished if it is still deemed as being in a ‘running’ state!

I tried to kill hvced.exe using a script (I tried taskkill.exe and Powershell’s Stop-Process) which ran in the ExitProcess scripting phase of the UserConfig.xml.  Unfortunately the context that these scripts run in didn’t have enough permissions to kill the process, since EntDistributorService.exe was launched in a SYSTEM context and as a result hvced.exe was also running in a SYSTEM context.

After all this fiddling around, it turned out that the solution was quite simple really.  In the DeploymentConfig.xml file we can simply add the parent file who’s child processes we want to terminate like so:

<TerminateChildProcesses>      
    <Application Path="[{AppVPackageRoot}]\VFS\ProgramFilesCommonX86\HealthVISION\EntDistributorService.exe" />         
</TerminateChildProcesses>

And it worked like a charm.

Rename App-V 4.6 Package Before Converting to App-V 5.1

I’ve been converting a bunch of apps recently from App-V 4.6 format to App-V 5.1 format.  I knew this could be done using PowerShell cmdlets, so rather than reinvent the wheel I used a great script written by Tim Mangan to do this for me.

The problem I had was that our App-V 4.6 packages all had a specific naming convention that included the characters ‘AV46’…..or worse….didn’t have a consistent naming convention whatsoever!  So it meant that when these packages were converted to App-V 5.1 they either still contained the characters ‘AV46’ (which looked stupid in my App-V 5.1 console) or were named inconsistently.

Since the ConvertFrom-AppvLegacyPackage cmdlet doesn’t expose a way of naming the ‘output’ package, and I didn’t want to open and re-save each of my converted App-V 5.1 packages to merely change the package name, the only option I saw was to rename the App-V 4.6 package before I converted it!  So that’s exactly what this script does!

Why is it written in VBScript and not PowerShell?  Simply so we can drag a package folder onto the .vbs file and rename it.  Otherwise to do the same for PowerShell would require hacking around in the registry.

How it works

Consider you have a folder containing the following App-V 4.6 package:

App-V 4.6 package

First you should grab the script below, and save it as (for example) RenameAV46.vbs.

You would then simply drag the parent package folder (Gateway_WinDip_2.2_32_AV46_R01) on to RenameAV46.vbs.  It will then prompt you for a new package name, where you would specify the new package name (for example) ‘Gateway_WinDip_2.2_32_AV51_R01’ or similar.

The script renames the minimal amount required to prepare your App-V 4.6 package for App-V 5.1 conversion.  For example, it will not rename the icons folder and re-point the OSD shortcut icons since this is not required for the conversion process to be successful.

Once you have renamed the package, it is then ready to run through Tim’s conversion script!

Option Explicit

Dim objFSO : Set objFSO = createobject("Scripting.FilesystemObject")

Dim objXMLDoc : Set objXMLDoc = CreateObject("Microsoft.XMLDOM") 
objXMLDoc.async = False 

Dim objXMLDoc2 : Set objXMLDoc2 = CreateObject("Microsoft.XMLDOM") 
objXMLDoc2.async = False 
	
Dim folderPath : folderPath = wscript.arguments(0)
Dim oldPackageName : oldPackageName = ""
Dim currentFilePath : currentFilePath = ""
Dim currentFileName : currentFileName = ""
Dim currentParentFilePath : currentParentFilePath = ""
Dim currentFileExtension: currentFileExtension = ""

Dim newFilePath : newFilePath = ""
Dim newFileName : newFileName = ""

Dim existingPATH : existingPATH = ""
Dim newPATH : newPATH = ""
Dim existingHREF : existingHREF = ""
Dim newHREF : newHREF = ""
Dim existingNAME : existingNAME = ""
Dim newNAME : newNAME = ""

Dim newPackage : newPackage = ""

Dim parentFolderPath : parentFolderPath = objFSO.GetParentFolderName(folderPath)
Const ForReading = 1
Const ForWriting = 2		

Public Function DecodeXml(TextToDecode)
    'Take text that has been encoded for XML and change it to normal text
    Dim Res
    Res = Replace(Replace(Replace(Replace(TextToDecode, "&quot;", """"), "&gt;", ">"), "&lt;", "<"), "&amp;", "&")
    DecodeXml = Res
End Function

Public Function EncodeXml(TextToEncode)
    'Take text and change special chars so that it can be included in an XML file
    Dim Res
    Res = Replace(Replace(Replace(Replace(TextToEncode, "&", "&amp;"), ">", "&gt;"), "<", "&lt;"), """", "&quot;")
    EncodeXml = Res
End Function

Function GetNewFilePath(existingFilePath, oldPackageName, newPackageName)	
	'get new file name based on old file name, stripping out any version ("_!","_£" etc)	
	Dim RegX : Set RegX = new RegExp
				
	RegX.Pattern = "(" & oldPackageName & ")(_\d+)*"
	RegX.Global = True

	GetNewFilePath = RegX.Replace(existingFilePath, newPackageName)	
End Function

Sub RenameFile(currentFilePath, newFilePath)
	If (currentFilePath <> newFilePath) Then
		objFSO.MoveFile currentFilePath, newFilePath
		currentFilePath = newFilePath		
	End if			
End Sub

Dim newPackageName : newPackageName = InputBox("Enter new package name")

If (trim(newPackageName) = "") Then
	MsgBox "The package name was blank - please populate.  Quitting."
	wscript.quit
End If

Dim newPackagePath : newPackagePath = parentFolderPath & "\" & newPackageName

'rename package folder with new name
objFSO.MoveFolder folderPath, newPackagePath

Dim newPackagePathFolder : Set newPackagePathFolder = objFSO.GetFolder(newPackagePath)
Dim nodetext,folder,file

'get SPRJ (project) name
For Each file In newPackagePathFolder.Files
	Select Case UCase(objFSO.GetExtensionName(file.Path))
		Case "SPRJ"
			oldPackageName = objFSO.GetBaseName(file.Path)
			Exit For
	End Select
Next

'iterate though package files
For Each file In newPackagePathFolder.Files
	
	currentFilePath = file.Path
	
	'We need to be careful of the encoding in the DEFAULTOSD element
	Select Case UCase(objFSO.GetExtensionName(currentFilePath))
	Case "MSI","XML"				
		'just rename these file types
		newFilePath = GetNewFilePath(currentFilePath, oldPackageName, newPackageName)	
		RenameFile currentFilePath, newFilePath	
	Case "SPRJ"		
		
		objXMLDoc.load(currentFilePath)
		
		If 0 <> objXMLDoc.ParseError Then
			msgbox "SPRJ " & objXMLDoc.ParseError.Reason & " " & currentFilePath
		Else		
			'change PACKAGEFILE
			Dim PackageFile : Set PackageFile = objXMLDoc.selectSingleNode(".//SEQUENCERPROJECT/PACKAGEFILE")
			If Not PackageFile Is Nothing Then	  
				existingPATH = PackageFile.getAttribute("Path")		

				'we rename the SFT in here instead of in the SELECT...CASE statement in case there are multiple SFTs (messy package folder)
				newFilePath = GetNewFilePath(newPackagePathFolder & "\" & existingPATH, oldPackageName, newPackageName)
				RenameFile newPackagePathFolder & "\" & existingPATH, newFilePath				
				
				newPATH = GetNewFilePath(existingPATH, oldPackageName, newPackageName)	
				PackageFile.setAttribute "Path",newPATH
				objXMLDoc.Save(currentFilePath)	
			Else
				Msgbox "cannot find PACKAGEFILE node"
			end if
			
			Set PackageFile = Nothing				
			
			'the DEFAULTOSD node is actually encoded XML, so here we read it all in so we can parse the XML DOM and update CODEBASE HREF and PACKAGE NAME
			
			Dim DefaultOSDNode : Set DefaultOSDNode = objXMLDoc.selectSingleNode(".//SEQUENCERPROJECT/DEFAULTOSD")
			If Not DefaultOSDNode Is Nothing Then
			
				'remember that the DEFAULTOSD node contains XML itself, so we re-load the contents into the XML parser
				nodetext = DefaultOSDNode.text

				objXMLDoc2.loadXML(nodetext)
				
				If 0 <> objXMLDoc2.ParseError Then
					msgbox "DEFAULTOSD CODEBASE " & objXMLDoc2.ParseError.Reason
				Else
				
					Dim CodebaseNode : Set CodebaseNode = objXMLDoc2.selectSingleNode(".//SOFTPKG/IMPLEMENTATION/CODEBASE")
					If Not CodebaseNode Is Nothing Then	  
						existingHREF = CodebaseNode.getAttribute("HREF")
						newHREF = GetNewFilePath(existingHREF, oldPackageName, newPackageName)				
						CodebaseNode.setAttribute "HREF",newHREF							
					Else
						Msgbox "cannot find CODEBASE node"
					end if
					
					Set CodebaseNode = Nothing	
					
					Dim PackageNode : Set PackageNode = objXMLDoc2.selectSingleNode(".//SOFTPKG/PACKAGE")
					If Not PackageNode Is Nothing Then
						PackageNode.setAttribute "NAME",newPackageName							
					Else
						Msgbox "cannot find PACKAGE node"
					end if
					
					Set PackageNode = Nothing			
									
				End If			
				'set new value for DEFAULTOSD contents
				DefaultOSDNode.text = objXMLDoc2.selectSingleNode(".//SOFTPKG/").xml
				objXMLDoc.Save(currentFilePath)	
			Else
				Msgbox "cannot find DEFAULTOSD node"
			End If					
			Set DefaultOSDNode = Nothing	
		End If	
		
		newFilePath = GetNewFilePath(currentFilePath, oldPackageName, newPackageName)	
		RenameFile currentFilePath, newFilePath
		
	Case "OSD"
		
		objXMLDoc.load(currentFilePath)
		
		If 0 <> objXMLDoc.ParseError Then
			msgbox "OSD " & objXMLDoc.ParseError.Reason & " " & currentFilePath
		Else		
			Set CodebaseNode = objXMLDoc.selectSingleNode(".//SOFTPKG/IMPLEMENTATION/CODEBASE")
			If Not CodebaseNode Is Nothing Then	   
				existingHREF = CodebaseNode.getAttribute("HREF")
				newHREF = GetNewFilePath(existingHREF, oldPackageName, newPackageName)				
				CodebaseNode.setAttribute "HREF",newHREF
				objXMLDoc.Save(currentFilePath)	
			Else
				Msgbox "cannot find CODEBASE node"
			end if
			
			Set CodebaseNode = Nothing				
		End If			
	End Select	
Next

MsgBox "Done"

Set objXMLDoc = Nothing
Set objFSO = Nothing

 

 

Start an App-V 5 Process immediately after Publishing

I have a piece of software that acts as an ‘agent’ sitting in the background (in the System Tray) waiting to intercept print requests.  It needs to be running all the time since it intercepts print requests from ANY desktop application.

The target platforms are a combination of standard desktops and non-persistent VDI desktops.  Making it work with standard desktops wasn’t really the problem – by default the software placed a shortcut in the Startup folder and after publishing, logging off and logging in the background process started.

The trouble with a non-persistent VDI is that when a user logs off and logs back in, they get a completely fresh desktop experience and all the apps get published again.  As a result of this we can’t just lump some logic in a Run key or the Startup folder since the application is published after these features are processed.

Combined with this there were another 2 applications that needed connection-grouping with this agent application.

Unless there’s a more elegant way of achieving this that I’m not aware of, I present my solution forthwith:

Firstly, since we’re publishing to users, I have added a user script at publish time in the AppxManifest.xml file (or you could use the UserConfig.xml file – your choice depending on which version of App-V you’re using):

		
<appv:UserScripts>
        <appv:PublishPackage>
            <appv:Path>cmd.exe</appv:Path>
            <appv:Arguments>/c START "" "powershell.exe" -ExecutionPolicy ByPass -WindowStyle Hidden -File "[{AppVPackageRoot}]\..\Scripts\runProcess.ps1"</appv:Arguments>
            <appv:Wait RollbackOnError="false" />
        </appv:PublishPackage>
    </appv:UserScripts>
	

What’s important to note is that we can’t just point at our background process for 2 reasons:

  1. Because the application is not yet published at this point, and so we can’t access/run it
  2. Even if we could access/run it, the publishing process would stick at running the .exe since the .exe will always be running and the PublishPackage action will never release the handle (unless we manually kill it).  And hence the package would never publish!

Instead we’ve called cmd.exe with the START command.  What this does is launch powershell.exe as a separate process and does NOT wait for it to complete.  Hence the publishing of the app will complete as normal.  Meanwhile in the background, powershell.exe runs a script that I’ve placed in the Scripts folder in the background.  So what does the script do?  In essence it:

  • Loops 10 times maximum, with a 5 second delay between each loop, and attempts to get the package (Get-AppvClientPackage) and ensure it is published to the user (IsPublishedToUser).  We cannot run the process if the package is not published.
  • Loops 10 times maximum, with a 5 second delay between each loop, and attempts to get the connection group (Get-AppvClientConnectionGroup) and ensure it is enabled (IsEnabledToUser).  If using a connection group, we cannot enable a connection group if a package is in use!  So we need to wait until the connection group is enabled before we attempt to start the process.
  • Loops 10 times maximum, with a 5 second delay between each loop, and attempts to start the process (Start-Process).  There’s no reason why it shouldn’t start first time, but I still add in some retries just in case.

Here is the script (runProcess.ps1)

#variable to store the current script folder (in case, for ease, we want to put our exe's in the same folder as the script)
$ScriptFolder = $PSScriptRoot

#path that points to the 'Root' folder inside an app-v package
$appvRoot = split-path $ScriptFolder -parent
$appvRoot = $appvRoot + "\Root"

#default number of attempts
$attempts = 10
#path to exe inside app-v package
$processPath = "$appvRoot\VFS\ProgramFilesX86\Equitrac\Express\Client\EQMsgClient.exe"
#process name without exe (used with Get-Process)
$processName = "EQMsgClient"
#name of App-V package
$packageName = "Nuance_EquitracExpressClient_5.4.23.4801"
#name of connection group
$connectionGroupName = "Equitrac Express"

#initialise
$packageObject = $false
$connectionGroupObject = $false
$newProcess = $null

$eventMessage = "Attempting to run: " + $processPath
write-host $eventMessage


#wait until package is published
$packageObject = Get-AppvClientPackage -Name $packageName -ErrorAction SilentlyContinue

while (($packageObject -eq $null -Or (!($packageObject.IsPublishedToUser))) -And $attempts -gt 0)
{   
    Start-Sleep -s 5
    $attempts = $attempts - 1

    $eventMessage = "Checking if the following package is published: " + $packageName + " (Attempts remaining: " + $attempts + ")"
    write-host $eventMessage  

    $packageObject = Get-AppvClientPackage -Name $packageName -ErrorAction SilentlyContinue  
}


if ($packageObject.IsPublishedToUser)
{
    $eventMessage = "The following package is published: " + $packageName
    write-host $eventMessage

    #now we need to check that the connection group has applied and is published!
    $connectionGroupObject = Get-AppvClientConnectionGroup -Name $connectionGroupName -ErrorAction SilentlyContinue
    
    #reset attempts
    $attempts = 10

    while (($connectionGroupObject -eq $null -Or (!($connectionGroupObject.IsEnabledToUser))) -And $attempts -gt 0)
    {   
        Start-Sleep -s 5
        $attempts = $attempts - 1

        $eventMessage = "Checking if the following connection group is enabled: " + $connectionGroupName + " (Attempts remaining: " + $attempts + ")"
        write-host $eventMessage

        $connectionGroupObject = Get-AppvClientConnectionGroup -Name $connectionGroupName -ErrorAction SilentlyContinue
    }

    
    if ($connectionGroupObject.IsEnabledToUser)
    {
        $eventMessage = "The following connection group is enabled: " + $connectionGroupName
        write-host $eventMessage

        #reset attempts
        $attempts = 10

        #now try and start our process
        while (((Get-Process -Name $processName -ErrorAction SilentlyContinue) -eq $null) -And $attempts -gt 0)
        {
            if (Test-Path $processPath)
            {
                $newProcess = Start-Process -FilePath $processPath -NoNewWindow -ErrorAction SilentlyContinue       
            }
            else
            {
                $eventMessage = "Process path : " + $processPath + " was not found.  Unable to start process."
                write-host $eventMessage
                break
            }
            $attempts = $attempts - 1

            $eventMessage = "Trying to start process: " + $processPath + " (Attempts remaining: " + $attempts + ")"
            write-host $eventMessage

            Start-Sleep -s 5
        }
        
        if ($newProcess -eq $null)
        {
            $eventMessage = "Process : " + $processPath + " could not be launched."
            write-host $eventMessage
        }
        else
        {
            $eventMessage = "Process : " + $processPath + " was launched successfully."
            write-host $eventMessage
        }
        
    }
    else
    {
        $eventMessage = "Connection Group : " + $connectionGroupName + " was not enabled.  Unable to start process."
        write-host $eventMessage
    }
}
else
{
    $eventMessage = "Package : " + $packageName + " was not published.  Unable to start process."
    write-host $eventMessage
}

Things to tweak

The variables at the top (package names etc).  Also remember this targets packages published to the user, not the machine.  So you’ll need to tweak things like ‘isEnabledToUser’ and ‘isPublishedToUser’ etc.  Also you may not be using a connection group like me, so you could chop that logic out too.

A note before you commence…

Since the background process is always running, it obviously can’t be unpublished instantly (because the package will be in use).  Luckily we’re running the App-V 5 SP2 client which will set up a pending task to un-publish it when the user logs off and back in.  You may want to consider this before implementing it.

Using Taskkill.exe to Kill Multiple Processes on One Line

I’ve had an issue with my current App-V 5 application.  When I launch an executable called CentralSurveillance.exe, it spawns another two processes called K2DataLogger.exe and K2MSService.exe.  And these remain open even when I close CentralSurveillance.exe.  My first thoughts were that these were child processes, so I added the following to the DeploymentConfig:

<TerminateChildProcesses>
<Application Path="[{ProgramFilesX86}]\K2 Medical Systems\Guardian\CentralSurveillance.exe" />
</TerminateChildProcesses>

Sure enough, when I closed the instance of CentralSurveillance.exe….the instances of K2DataLogger.exe and K2MSService.exe were still running!  So I dug a little deeper – were these actually child processes or not?

I opened up a command prompt and ran:

wmic process Get Name,Description,CommandLine,ProcessId,ParentProcessId /format:list > processes.txt

What this does is list the name, description, command line, process id and parent process id of each running process and outputs it to a text file called processes.txt.  The results (after I’d deleted the irrelevant stuff) were as follows:

CommandLine="C:\Program Files\Microsoft Application Virtualization\Client\AppVClient.exe"
Description=AppVClient.exe
Name=AppVClient.exe
ParentProcessId=496
ProcessId=2664

CommandLine=C:\windows\system32\svchost.exe -k DcomLaunch
Description=svchost.exe
Name=svchost.exe
ParentProcessId=496
ProcessId=612

CommandLine=C:\windows\Explorer.EXE
Description=explorer.exe
Name=explorer.exe
ParentProcessId=4032
ProcessId=4224

CommandLine="C:\Users\xxxxx\AppData\Local\Microsoft\AppV\Client\Integration\07390066-7746-41A4-B6FB-3D1977D289BA\Root\VFS\ProgramFilesX86\K2 Medical Systems\Guardian\CentralSurveillance.exe"
Description=CentralSurveillance.exe
Name=CentralSurveillance.exe
ParentProcessId=4224
ProcessId=6120

CommandLine="C:\ProgramData\App-V\07390066-7746-41A4-B6FB-3D1977D289BA\CF8C5523-5038-4C73-991E-EBB4812B110B\Root\VFS\ProgramFilesX86\K2 Medical Systems\Guardian\K2DataLogger.exe"
Description=K2DataLogger.exe
Name=K2DataLogger.exe
ParentProcessId=2664
ProcessId=6040

CommandLine="C:\ProgramData\App-V\07390066-7746-41A4-B6FB-3D1977D289BA\CF8C5523-5038-4C73-991E-EBB4812B110B\Root\VFS\ProgramFilesX86\K2 Medical Systems\Guardian\K2MSService.exe"
Description=K2MSService.exe
Name=K2MSService.exe
ParentProcessId=2664
ProcessId=3872

We can clearly see that the parent process of K2DataLogger.exe and K2MSService.exe is NOT CentralSurveillance.exe (process id 6120) but instead AppVClient.exe (process id 2664).  And this is probably why TerminateChildProcesses will not work in this instance.  The workaround I used is to run an ExitProcess script so that when we close CentralSurveillance.exe, it kills both K2DataLogger.exe and K2MSService.exe.

I did this like so:

<UserScripts>
<ExitProcess>
<Path>CMD.EXE</Path>
<Arguments>/C taskkill /im "K2DataLogger.exe" /f /t &amp; taskkill /im "K2MSService.exe" /f /t</Arguments>
<Wait RollbackOnError="false" />
<ApplicationId>[{ProgramFilesX86}]\K2 Medical Systems\Guardian\CentralSurveillance.exe</ApplicationId>
</ExitProcess>
</UserScripts>

You can see that I can specify multiple command lines to run via CMD.exe by separating each command by an ampersand (& – this needs to be URL encoded in an XML file which is why it would appear as &amp; instead).  In the workaround above I specify two processes.  However by using brackets and nesting commands, I can specify more!  Here’s an example of an argument that kills 4 processes:

taskkill /im "odbcad32.exe" /f /t & (taskkill /im "wordpad.exe" /f /t & (taskkill /im "notepad.exe" /f /t & taskkill /im "mspaint.exe" /f /t))

Using RunVirtual to Present Excel Addins

I’ve recently been having a look at the best way to present Excel Add Ins using our locally installed instance of Microsoft Excel and our virtualised Add Ins.

The easiest approach is probably to create an additional shortcut in your package called ‘Microsoft Excel 2010 with XXX Add In’ which points to your local instance of Excel.exe.  Running this shortcut will launch the local instance of Excel.exe inside the virtual bubble, and you should then see your virtualised Add Ins when the application is loaded.

The trouble with this approach is that users will generally head straight to the main shortcut to Microsoft Excel (the shortcut to the local instance) and wonder why they can’t see their Add In.  On top of this, using the approach above could mean that we have multiple ‘Microsoft Excel 2010 with XXX Add In’ shortcuts, which can look quite untidy.

An alternative approach is presented by the RunVirtual registry key introduced in App-V 5 .  What this feature enables us to do is to launch the shortcut to the locally installed version of Excel and drag this process (Excel.exe) instance inside the virtual bubble of a specified package.

Again, I won’t go into too much detail here since there’s a great blog about how RunVirtual works here and also a mention of an important update for App-V 5 SP3 here, which enables us to target packages published to the User.

Anyway, I’m babbling a bit so we’ll crack on….

In my opinion RunVirtual is ALMOST great.  Ideally we would be able to create a RunVirtual reference to a local application like so (Microsoft Excel in this example):

HKEY_CURRENT_USER\Software\Microsoft\AppV\Client\RunVirtual\Excel.exe

and then list multiple virtualised Addins underneath that (by specifying their PackageId and VersionId).  Unfortunately we can only specify one virtual package under this key.

What this limitation means is that we will need to specify one ’empty’ virtual package which maintains a static PackageID and VersionId.  This package will NEVER change.  We will then use a Connection Group to connect this single package to our multiple virtualised Add In packages.  What this essentially means is that when we launch the local version of Excel.exe, it will launch our associated RunVirtual package (our ’empty’ package) which in turn will launch all of its connected packages.

In honesty though, and before we continue, you would really require App-V 5 SP3 as a minimum.  Why?  Because the schema for connection groups has changed and it now enables us to specify OPTIONAL packages (i.e, if a connected package is not published just ignore it and don’t throw an error!) and to automatically use the latest published version of a package.  For me, the ‘optional’ addition is the most important.  After all, our Add Ins aren’t necessarily for all users!  We may have one Add In policied to a specific group of users, and another Add In policied to another specific group of users.  And membership of these policied AD Groups may very well be mutually exclusive!  The optional attribute saves us a great headache in that sense and provides us with a flexibility that helps to reduce the management overhead.

Am I still babbling?  Ok.  An example….

Step 1.  Create an ‘Empty’ Package

We will link to this from the RunVirtual key.  This package isn’t entirely empty – make sure you add a dummy file to the package otherwise it will throw an error when you’re adding the package (presumably because there is no VFS to mount).

Also, ensure you allow Named Objects and COM objects to interact with the local system.  They’re not both required for this example (although I think Named Objects IS required), but we need to think ahead.  If we are packaging a COM Add In for Excel, this may well be required.

Finally follow this guide.  This guide lets us dynamically add the relevant RunVirtual key from our AppxManifest.xml.  In this example we are going to add RunVirtual in the HKCU key, since we are publishing the package to the User and NOT the machine.

Note to SCCM 2012 Virtual Environment users (and this part is all untested by myself):

As mentioned in the comments section below, when using SCCM 2012 Virtual Environments it is not a requirement that you specify a dummy package. Instead you can specify the AppConnectionGroupId and VersionId of the connection group. The problem with this approach is that the connection group gets dynamically generated on each client, which means that the AppConnectionGroupId and VersionId of the connection group is different for each client. I have suggested that to set the RunVirtual registry value dynamically you can use the approach documented here, so long as the dynamically generated connection group has a static name (I can’t verify this because I don’t have the environment to test with)!!

Get-AppvClientConnectionGroup -all | where {$_.Name -eq 'Name_Of_Connection_Group'} | foreach { New-Item -Path HKCU:\SOFTWARE\Microsoft\AppV\Client\RunVirtual -Name Excel.exe -Value ($_.AppConnectionGroupId.toString() + '_' + $_.VersionId.toString()) -Force }

The alternative approach is to use a dummy package like I have suggested in this blog, whereby the PackageId and VersionId of the dummy package remains static.

Another Note:

As Tim Mangan has mentioned in the comments section below, delivery via an App-V server differs from SCCM 2012 Virtual Environments in that it requires at least one non-optional package.

Step 2.  Sequence Two Add In Packages

Each package will contain a different Excel Add In.  For this proof of concept I Googled for free XLA files and created a package for each one.  I simply lumped the XLA file into the following location, and that was that:

%AppData%\Microsoft\Excel\XLSTART

(You can alternatively copy it here for a per-machine install)
C:\Program Files\Microsoft Office\Office14\XLSTART

Again, ensure you allow Named Objects and COM objects to interact with the local system.  We need to ensure that all of our connected packages contain the same setting for COM and Named Objects, otherwise we may get errors when launching the connection group such as:

The Application Failed to Launch.  This may be due to a network failure
or
The connection group {xxx} version {xxx} could not be published because the virtual COM settings of the individual packages conflict.
Verify that the virtual COM settings are the same for all member packages and try again.

Step 3 – Create Connection Group for Local Testing

Create a connection group using this tool.  You should add the ’empty’ package as being mandatory (i.e, NOT optional).  The version shouldn’t matter since this package will NEVER change.

The other two Add In packages should be marked as Optional and Ignore Version.

Connection Group(Note that in your live environment you will probably just use your App-V publishing server to manage this connection group and its settings)

Step 4 – Local Testing

Ensure scripting is enabled on your machine

Set-AppvClientConfiguration -EnablePackageScripts 1

Add and Publish all your packages (since our RunVirtual key should be installing to HKCU, we publish our packages to the User and NOT the machine)

Add-AppvClientPackage -Path "Empty_Package.appv" | Publish-AppvClientPackage
Add-AppvClientPackage -Path "Addin1.appv" | Publish-AppvClientPackage
Add-AppvClientPackage -Path "Addin2.appv" | Publish-AppvClientPackage

Add and Enable our Connection Group

Add-AppvClientConnectionGroup -path "AddInCG.xml" | Enable-AppvClientConnectionGroup

Launch the local Excel version, and see your Add Ins appear!

As a further test, try un-publishing just one of the Add In packages and then launch Excel again.  You will notice 2 things:

1)  You don’t get an error, even though the package is not published!  This is due to the Optional attribute in our connection group.  So that works well.

2)  You will notice that the Add In still appears in the Excel Add In tab!  Doh.  Why has this happened?

Well.  Putting App-V aside for a second.  You will notice that when you lump your XLA file into %AppData%\Microsoft\Excel\XLSTART and then launch Excel, the date modified of the XLA file changes!  Close Excel and the Date Modified reverts back to its original Date Modified date.  So what I believe is happening here is that App-V detects a modification to the file, and writes it to a Copy on Write location.

Importantly, it doesn’t write it to the Copy on Write location of the Add In package but instead to the Copy and Write location of the Connection Group!!

If you copied your XLA file to the per-user location (as in this example), you will find a saved XLA file in this location:

(Copy on Write (COW) Roaming) %AppData%\Microsoft\AppV\Client\VFS\{ConnectionGroupPackageId}\AppData\Microsoft\Excel\XLSTART

If you copied your XLA file to the per-machine location, you will find a saved XLA file in this location:

(Copy on Write (COW) Local) %LocalAppData%\Microsoft\AppV\Client\VFS\{ConnectionGroupPackageId}\ProgramFilesX86\Microsoft Office\Office14\XLSTART

So in essence, when you un-publish your Add In packages you will need to add some logic to delete this file.

Configuring RunVirtual Registry from the AppxManifest.xml

To configure the RunVirtual key from an App-V package we needed to add a registry key/value to the local machine via an App-V script.  Since our environment is VDI we are using a new feature in App-V 5 SP3 which enables us to target an App-V application which is published to the user (and not to the machine).

I won’t discuss what RunVirtual is – you can read this if you need to know.

The key reason for this tutorial is because we are performing this logic in the AppxManifest.xml and NOT the UserConfig.xml.  I prefer to add my logic to AppxManifest.xml since it makes my package self-contained with no reliance on external (xml) files – see here.

Why is adding RunVirtual registry logic to the AppxManifest.xml different from adding it to the UserConfig.xml?

Well first a very brief bit of background – the registry key/value we need to add is:

HKCU\SOFTWARE\Microsoft\AppV\Client\RunVirtual\{LocalProcess.exe}

(Default)     REG_SZ     {PackageId}_{VersionId}

By creating a key under RunVirtual with the process name (Excel.exe in this case) we effectively saying “whenever we run Excel.exe I want it to run in the same virtual space as the App-V package with the specified PackageId_VersionId”.  As you’ve probably gathered, this specific package was an Addin for Microsoft Excel.

Adding the logic via a UserConfig.xml script is relatively trivial.  We can open the XML file in a text editor and add the following:

   <UserScripts>
      <PublishPackage>
        <Path>cmd.exe</Path>
        <Arguments>/C REG ADD HKCU\SOFTWARE\Microsoft\AppV\Client\RunVirtual\Excel.exe /ve /d "2a04d2aa-ad7a-4c51-b774-00ca0e1e1fad_e2ce036d-9cbf-479d-9b67-8b4648742e8c" /f</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
      </PublishPackage>
      <UnpublishPackage>
        <Path>cmd.exe</Path>
        <Arguments>/C REG DELETE HKCU\SOFTWARE\Microsoft\AppV\Client\RunVirtual\Excel.exe /f</Arguments>
        <Wait RollbackOnError="false" Timeout="30"/>
      </UnpublishPackage>
    </UserScripts>

However adding a similar change via the AppxManifest.xml is slightly more complex.  Whenever we import a new AppxManifest.xml via the Advanced tab in Edit mode, we need to save the package afterwards for the change to take effect.  And what does this do?  It flips the VersionId of the package!  So we’re left with a situation whereby whenever we update the VersionId in the AppxManifest.xml it becomes redundant as soon as we save the updated package!

Introducing the Powershell approach….

Ignore the slightly different XML syntax for now – this is irrelevant in the context of this tutorial.

<appv:UserScripts>
<appv:PublishPackage>
<appv:Path>powershell.exe</appv:Path>
<appv:Arguments>-ExecutionPolicy ByPass -command "Get-AppvClientPackage -all | where {$_.Name -eq 'Alkane_Package'} | foreach { New-Item -Path HKCU:\SOFTWARE\Microsoft\AppV\Client\RunVirtual -Name Excel.exe -Value ($_.PackageId.toString() + '_' + $_.VersionId.toString()) -Force }"</appv:Arguments>
<appv:Wait RollbackOnError="false" />
</appv:PublishPackage>
<appv:UnpublishPackage>
<appv:Path>powershell.exe</appv:Path>
<appv:Arguments>-ExecutionPolicy ByPass -command "Remove-Item -Path HKCU:\SOFTWARE\Microsoft\AppV\Client\RunVirtual\Excel.exe -Recurse -Force"</appv:Arguments>
<appv:Wait RollbackOnError="false" />
</appv:UnpublishPackage>
</appv:UserScripts>

The main part to note is the Powershell one-liner, which I’ll explain below:

Get-AppvClientPackage -all | where {$_.Name -eq 'Alkane_Package'} | foreach { New-Item -Path HKCU:\SOFTWARE\Microsoft\AppV\Client\RunVirtual -Name Excel.exe -Value ($_.PackageId.toString() + '_' + $_.VersionId.toString()) -Force }

What we’re doing here is:

  • Getting all locally ADDED packages (Note that Get-AppvClientPackage -Name “Alkane_Package” will return nothing since the package is not published when the PublishPackage event runs!)
  • Looping through them where the Name attribute is the name of my target package (Alkane_Package)
  • We then use foreach because of this
  • And then we can dynamically get the PackageId and VersionId of the package to add it to the registry value

Scripting Examples for App-V 5

Below are a few scripting examples for App-V 5 configuration files (DeploymentConfig, UserConfig and AppxManifest files – the XML syntax varies slightly between these files but the same principle applies).  They’re purely for illustrative purposes and aim to show how we can string together the executable and their arguments to achieve different things.

**UPDATED WORKAROUND FOR MSIEXEC – Scroll down…**

Installing a shim

<MachineScripts>              
    <AddPackage>        
        <Path>sdbinst.exe</Path>        
        <Arguments>/q "[{AppVPackageRoot}]\..\Scripts\ExampleFile.sdb"</Arguments>      
        <Wait RollbackOnError="true" Timeout="30"/>      
    </AddPackage>   
</MachineScripts>

Running a Powershell script

<MachineScripts>
<AddPackage>
<Path>powershell.exe</Path>
<Arguments>-ExecutionPolicy ByPass -WindowStyle Hidden -File "[{AppVPackageRoot}]\..\Scripts\ExampleFile.ps1"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Running a Powershell command

<MachineScripts>
<AddPackage>
<Path>powershell.exe</Path>
<Arguments>-ExecutionPolicy ByPass -WindowStyle Hidden -Command  "&amp; { Get-EventLog -LogName security }"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Running an EXE (For example, Powershell) without waiting for execution to complete

<MachineScripts>
<AddPackage>
<Path>cmd.exe</Path>
<Arguments>/c START "" "powershell.exe" -ExecutionPolicy ByPass -WindowStyle Hidden -File "[{AppVPackageRoot}]\..\Scripts\runProcess.ps1"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Running a VBScript (via the CScript engine)

<MachineScripts>
<AddPackage>
<Path>cscript.exe</Path>
<Arguments>"[{AppVPackageRoot}]\..\Scripts\ExampleFile.vbs"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Running a VBScript (via the WScript engine)

<MachineScripts>
<AddPackage>
<Path>wscript.exe</Path>
<Arguments>"[{AppVPackageRoot}]\..\Scripts\ExampleFile.vbs"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Installing an MSI (if this doesn’t work see workaround below!)

<MachineScripts>
<AddPackage>
<Path>msiexec.exe</Path>
<Arguments>/i "[{AppVPackageRoot}]\..\Scripts\ExampleFile.msi" /qb</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Installing a Driver

<MachineScripts>
<AddPackage>
<Path>pnputil.exe</Path>
<Arguments>/i /a "[{AppVPackageRoot}]\..\Scripts\ExampleFile.inf"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Adding a Registry String Value

<MachineScripts>
<AddPackage>
<Path>reg.exe</Path>
<Arguments>ADD HKLM\SOFTWARE\AlkaneTestKey /f /v AlkaneTestValue /t REG_SZ /d AlkaneTestData</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Giving Modify Permission to a Directory

<MachineScripts>
<AddPackage>
<Path>icacls.exe</Path>
<Arguments>"C:\AlkaneTest" /grant Users:(OI)(CI)(M) /C /Q</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

Creating a Symbolic Link (Running a windows command shell internal command – see here)

<MachineScripts>
<AddPackage>
<Path>cmd.exe</Path>
<Arguments>/C MKlink /D C:\AlkaneSolutions "C:\Program Files\AlkaneSolutions"</Arguments>
<Wait RollbackOnError="true" Timeout="30"/>
</AddPackage>
</MachineScripts>

 

**UPDATED WORKAROUND FOR MSIEXEC**

I referred a colleague of mine to this blog today.  He was trying to install an MSI from within the Scripts folder of his App-V 5 package.  But the example above didn’t work for him!!?  Event Viewer stated that MSIEXEC could not locate the MSI!?  I ran Process Monitor to confirm this.  And then I ran a quick local test.

I created two folders in the root of the C drive – C:\Scripts and C:\Root.  As you can probably tell, I was recreating the folder structure of an App-V 5 package to try and recreate the issue.

Essentially when we construct a path in an App-V 5 scripting node such as:

“[{AppVPackageRoot}]\..\Scripts\ExampleFile.msi”

When the package is added to a machine it will resolve to something like:

“C:\ProgramData\App-V\{PackageGUID}\{VersionGUID}\Root\..\Scripts\ExampleFile.msi”

Note how the path starts in the “Root” folder (“[{AppVPackageRoot}]”), goes up a level (“\..\”) and then goes into the Scripts folder.

To replicate this behaviour I placed a sample MSI in C:\Scripts called ExampleFile.msi and then opened up a command prompt and ran:

notepad.exe "c:\Root\..\Scripts\ExampleFile.msi"

The MSI opened in Notepad with no issues (full of gobbledygook as expected). I then ran:

msiexec.exe "c:\Root\..\Scripts\ExampleFile.msi"

Ooops!  The exact same path could not be resolved by the MSIEXEC engine!  So I assume MSIEXEC cannot handle the double-dot notation to go up one level in the directory structure?  As a workaround we had to run the MSI with a slightly different approach:

<MachineScripts>
    <AddPackage>
        <Path>cmd.exe</Path>
        <Arguments>/c cd "[{AppVPackageRoot}]\..\Scripts" &amp; msiexec.exe /i "ExampleFile.msi" /qn</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
    </AddPackage>
    <RemovePackage>
        <Path>cmd.exe</Path>
        <Arguments>/c cd "[{AppVPackageRoot}]\..\Scripts" &amp; msiexec.exe /x "ExampleFile.msi" /qn</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
    </RemovePackage>
</MachineScripts>

As ever, make a note of the timeout period (in seconds) if your MSI is likely to take longer to (un)install!  Also it might be worth setting RollbackOnError to “false” if your MSI is likely to request a (soft) reboot and return a 3010 (non-zero) error code!

AppxManifest.xml in place of DeploymentConfig.xml and UserConfig.xml

There’s a new feature in the App-V 5.1 Sequencer which enables us to export and import a file called AppxManifest.xml:

App-V 5.1 Sequencer Import/Export ManifestI’ve been using this approach recently to add custom scripting logic to my packages (I used ACE to make my life slightly easier).  What it means is that I can import my App-V package straight into the App-V Management Server and it works straight away with my custom changes – that is, I do not have to manually specify any Deployment/User config files using the approach below:

Appv-5 Overwrite ConfigIt also means that when I’m testing my application in standalone mode I do not have to specify DynamicUserConfigurationPath or DynamicDeploymentConfigurationPath parameters to apply the configurations to my package on the Powershell command line.

 

Common App-V 5 Powershell Commands

The following posts contains common App-V 5 Powershell commands.  Assuming a package consisting of the following:

Package Name: AlkaneSolutions
AlkaneSolutions.appv
AlkaneSolutions_DeploymentConfig.xml
AlkaneSolutions_UserConfig.xml

and a connection group consisting of:

Package Name: AlkaneSolutions_ConnectionGroup
AlkaneSolutions_ConnectionGroup.xml

General Commands

Import the App-V 5 module (so we can use the cmdlets below)

Import-Module AppvClient

Set execution policy to unrestricted (used during testing – should really be set by policy in a live environment)

Set-ExecutionPolicy unrestricted

Enable Package Scripting (can also be done via policy):

Set-AppvClientConfiguration -EnablePackageScripts 1

Package Commands

Add Package

Add-AppvClientPackage "<Path to package>\AlkaneSolutions.appv"

Add Package with a Deployment Config

Add-AppvClientPackage "<Path to package>\AlkaneSolutions.appv" -DynamicDeploymentConfiguration "<Path to package>\AlkaneSolutions_DeploymentConfig.xml"

Publish Package (-Global will publish globally.  Omitting this will publish to the user)

Publish-AppvClientPackage -Name "AlkaneSolutions" -Global

Mount the package at 100%

Mount-AppvClientPackage -Name "AlkaneSolutions"

Add, Publish and Mount Package on One Line

Add-AppvClientPackage "<Path to package>\AlkaneSolutions.appv" | Publish-AppvClientPackage -Global | Mount-AppvClientPackage

Add and publish App-V package along with a deployment xml

Add-AppvClientPackage "<Path to package>\AlkaneSolutions.appv" -DynamicDeploymentConfiguration "<Path to package>\AlkaneSolutions_DeploymentConfig.xml" | Publish-AppvClientPackage -Global

Add and publish App-V package along with a User Config xml (note how -Global is omitted from this command)

Add-AppvClientPackage "<Path to package>\AlkaneSolutions.appv" | Publish-AppvClientPackage -DynamicUserConfigurationPath "<Path to package>\AlkaneSolutions_UserConfig.xml"

Unpublish package

Unpublish-AppvClientPackage -name "AlkaneSolutions"

Delete App-V Package from cache

Remove-AppvClientPackage -name "AlkaneSolutions"

Connection Group Commands

Add Connection Group

Add-AppvClientConnectionGroup -path "<Path to package>\AlkaneSolutions_ConnectionGroup.xml"

Enable Connection Group (-Global will publish globally. Omitting this will publish to the user)

Enable-AppvClientConnectionGroup -name "AlkaneSolutions_ConnectionGroup" -Global

Add and Enable a Connection Group on One Line

Add-AppvClientConnectionGroup -path "<Path to package>\AlkaneSolutions_ConnectionGroup.xml" | Enable-AppvClientConnectionGroup -Global

Disable Connection Group

Disable-AppvClientConnectionGroup -name "AlkaneSolutions_ConnectionGroup"

Remove Connection Group

Remove-AppvClientConnectionGroup -name "AlkaneSolutions_ConnectionGroup"

Disable and Remove Connection Group on One Line

Disable-AppvClientConnectionGroup -name "AlkaneSolutions_ConnectionGroup" | Remove-AppvClientConnectionGroup

 Verification Commands

Get App-V Client Configuration Settings

Get-AppvClientConfiguration

View all added (not neccessarily published) packages (useful for retrieving names, GUIDs, publishing status etc)

Get-AppvClientPackage -all

View all added (not neccessarily published) connection groups  (useful for retrieving namesm GUIDs, publishing status etc)

Get-AppvClientConnectionGroup -all