App-V 5 Scripting Context

I carried out some simple App-V 5 scripting tests recently so that I could prove which context the various App-V 5 scripts run in.

I created a very simple App-V 5 package containing Dependency Walker, and added a shortcut to the executable located at [{ProgramFilesX86}]\DependencyWalker\depends.exe.

I also added a script to the Scripts folder called alkane.ps1:

param([string]$message)
add-content "c:\temp\alkane.log" "$message $([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")) $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)" -ErrorAction Stop

It’s a very simple PowerShell script that takes some text as an argument (I pass this to the script so that I know which scripting section it is running from).  Each time the script runs it will output the scripting section is has run from, whether the current scripting context is being run as an administrator, and also what account the script is running under.

In my DeploymentConfig.xml file I added the following UserScripts:

  <UserScripts>

      <StartProcess RunInVirtualEnvironment="true">
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig UserScripts StartProcess"</Arguments>
        <Wait RollbackOnError="true"/>
        <ApplicationId>[{ProgramFilesX86}]\DependencyWalker\depends.exe</ApplicationId>
      </StartProcess>

      <ExitProcess>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig UserScripts ExitProcess"</Arguments>
        <Wait RollbackOnError="false"/>
        <ApplicationId>[{ProgramFilesX86}]\DependencyWalker\depends.exe</ApplicationId>
      </ExitProcess>
      
      <StartVirtualEnvironment  RunInVirtualEnvironment="true">
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig UserScripts StartVirtualEnvironment"</Arguments>
        <Wait RollbackOnError="true"/>
      </StartVirtualEnvironment>
    
      <TerminateVirtualEnvironment>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig UserScripts TerminateVirtualEnvironment"</Arguments>
      <Wait RollbackOnError="false"/>
      </TerminateVirtualEnvironment>

      <PublishPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig UserScripts PublishPackage"</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
      </PublishPackage>

      <UnpublishPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig UserScripts UnpublishPackage"</Arguments>
        <Wait RollbackOnError="false" Timeout="30"/>
      </UnpublishPackage>
      
    </UserScripts>

and also the following MachineScripts:

 <MachineScripts>
      <PublishPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig MachineScripts PublishPackage"</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
      </PublishPackage>

      <UnpublishPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig MachineScripts UnpublishPackage"</Arguments>
        <Wait RollbackOnError="false" Timeout="30"/>
      </UnpublishPackage>
      
      <AddPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig MachineScripts AddPackage"</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
      </AddPackage>
      <RemovePackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "DeploymentConfig MachineScripts RemovePackage"</Arguments>
        <Wait RollbackOnError="false" Timeout="60"/>
      </RemovePackage>
  </MachineScripts>

and in the UserConfig.xml file I added the following UserScripts:

 <UserScripts>

      <StartProcess RunInVirtualEnvironment="true">
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "UserConfig UserScripts StartProcess"</Arguments>
        <Wait RollbackOnError="true"/>
        <ApplicationId>[{ProgramFilesX86}]\DependencyWalker\depends.exe</ApplicationId>
      </StartProcess>

      <ExitProcess>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "UserConfig UserScripts ExitProcess"</Arguments>
        <Wait RollbackOnError="false"/>
        <ApplicationId>[{ProgramFilesX86}]\DependencyWalker\depends.exe</ApplicationId>
      </ExitProcess>
      
      <StartVirtualEnvironment  RunInVirtualEnvironment="true">
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "UserConfig UserScripts StartVirtualEnvironment"</Arguments>
        <Wait RollbackOnError="true"/>
      </StartVirtualEnvironment>
    
      <TerminateVirtualEnvironment>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "UserConfig UserScripts TerminateVirtualEnvironment"</Arguments>
      <Wait RollbackOnError="false"/>
      </TerminateVirtualEnvironment>

      <PublishPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "UserConfig UserScripts PublishPackage"</Arguments>
        <Wait RollbackOnError="true" Timeout="30"/>
      </PublishPackage>

      <UnpublishPackage>
        <Path>powershell.exe</Path>
        <Arguments>-ExecutionPolicy ByPass -File "[{AppVPackageRoot}]\..\Scripts\alkane.ps1" -message "UserConfig UserScripts UnpublishPackage"</Arguments>
        <Wait RollbackOnError="false" Timeout="30"/>
      </UnpublishPackage>

    </UserScripts>

The Test

In chronological order I added the package, published the package, launched the application, closed the application, unpublished the package and removed the package. I specified various user/deployment XML permutations during add and publish time. The results can be seen below.

The Results

Test 1

  • Apply DeploymentConfig.xml
  • Do NOT apply UserConfig.xml
  • Publish to user
Script File Script Section Script Type Run As Admin Run As User
DeploymentConfig MachineScripts AddPackage True System
DeploymentConfig UserScripts PublishPackage False User
DeploymentConfig UserScripts StartVirtualEnvironment False User
DeploymentConfig UserScripts StartProcess False User
DeploymentConfig UserScripts ExitProcess False User
DeploymentConfig UserScripts TerminateVirtualEnvironment False User
DeploymentConfig UserScripts UnpublishPackage False User
DeploymentConfig MachineScripts RemovePackage True System

Test 2

  • Apply DeploymentConfig.xml
  • Do NOT apply UserConfig.xml
  • Publish to machine
Script File Script Section Script Type Run As Admin Run As User
DeploymentConfig MachineScripts AddPackage True System
DeploymentConfig MachineScripts PublishPackage True System
DeploymentConfig UserScripts StartVirtualEnvironment False User
DeploymentConfig UserScripts StartProcess False User
DeploymentConfig UserScripts ExitProcess False User
DeploymentConfig UserScripts TerminateVirtualEnvironment False User
DeploymentConfig MachineScripts UnpublishPackage True System
DeploymentConfig MachineScripts RemovePackage True System

Test 3

  • Do NOT apply DeploymentConfig.xml
  • Apply UserConfig.xml
  • Publish to user
Script File Script Section Script Type Run As Admin Run As User
UserConfig UserScripts PublishPackage False User
UserConfig UserScripts StartVirtualEnvironment False User
UserConfig UserScripts StartProcess False User
UserConfig UserScripts ExitProcess False User
UserConfig UserScripts TerminateVirtualEnvironment False User
UserConfig UserScripts UnpublishPackage False User

Test 4

  • Do NOT specify DeploymentConfig.xml
  • Apply UserConfig.xml
  • Publish to machine

Error – cannot specify UserConfig.xml if published to machine (-global)

Test 5

  • Apply DeploymentConfig.xml
  • Apply UserConfig.xml
  • Publish to user
Script File Script Section Script Type Run As Admin Run As User
UserConfig UserScripts PublishPackage False User
UserConfig UserScripts StartVirtualEnvironment False User
UserConfig UserScripts StartProcess False User
UserConfig UserScripts ExitProcess False User
UserConfig UserScripts TerminateVirtualEnvironment False User
UserConfig UserScripts UnpublishPackage False User
DeploymentConfig MachineScripts RemovePackage True System
UserConfig UserScripts PublishPackage False User
UserConfig UserScripts StartVirtualEnvironment False User
UserConfig UserScripts StartProcess False User
UserConfig UserScripts ExitProcess False User
UserConfig UserScripts TerminateVirtualEnvironment False User
UserConfig UserScripts UnpublishPackage False User

Test 6

  • Apply DeploymentConfig.xml
  • Apply UserConfig.xml
  • Publish to machine

Error – cannot specify UserConfig.xml if published to machine

Summary

A summary of these results is depicted in the table below:

Script Script Section Script Type Can Run Inside Virtual Environment? Potentially RUNs Context
DeploymentConfig UserScripts StartProcess Yes Multiple User
DeploymentConfig UserScripts ExitProcess No Multiple User
DeploymentConfig UserScripts StartVirtualEnvironment Yes Multiple User
DeploymentConfig UserScripts TerminateVirtualEnvironment No Multiple User
DeploymentConfig UserScripts PublishPackage No Multiple User/System*
DeploymentConfig UserScripts UnpublishPackage No Multiple User/System*
DeploymentConfig MachineScripts PublishPackage No Multiple System
DeploymentConfig MachineScripts UnpublishPackage No Multiple System
DeploymentConfig MachineScripts AddPackage No Once System
DeploymentConfig MachineScripts RemovePackage No Once System
UserConfig UserScripts StartProcess Yes Multiple User
UserConfig UserScripts ExitProcess No Multiple User
UserConfig UserScripts StartVirtualEnvironment Yes Multiple User
UserConfig UserScripts TerminateVirtualEnvironment No Multiple User
UserConfig UserScripts PublishPackage No Multiple User
UserConfig UserScripts UnpublishPackage No Multiple User

Additional Notes

  • Administrative privileges are required to publish globally, as such globally published packages will be published using the system account
  • You can not specify a UserConfig.xml file when publishing globally
  • The UserScripts in UserConfig.xml will override the UserScripts in DeploymentConfig.xml if both config files are applied
  • Only the StartProcess and StartVirtualEnvironment script types have the option to run inside the virtual environment.
  • Only AddPackage and RemovePackage run once in a package lifecycle.  Every other script type has the potential to run multiple times, including (Un)PublishPackage (for example, if we use Set-AppVClientPackage to specify a new config file we will need to re-publish the package, but not re-add it.)
  • * All UserScripts specified in the DeploymentConfig file will run in a user context when published to a user.  However, when published to a machine all UserScripts specified in the DeploymentConfig file will run in a user context with the exception of PublishPackage and UnpublishPackage, which will run under a system context.

 

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.