Desktop and Application Streaming

Bring your App-V packages to AppStream 2.0 with the dynamic application framework

Customers with existing Microsoft Application Virtualization (App-V) packages that are keen to use them with Amazon AppStream 2.0 can do so using the dynamic application framework. The dynamic application framework allows customers to build a custom application provider that dynamically adds applications to the AppStream 2.0 catalog.

This post details how to get started integrating App-V with AppStream 2.0 using the dynamic application framework.

Prerequisites

This tutorial assumes that you have the following installed and configured:

In addition to the preceding requirements, you must have a group policy object with the following settings deployed to the organizational units in which the computer objects for your AppStream 2.0 streaming instances reside:

  • Computer Configuration/Administrative Templates/System/App-V/Enable App-V Client
    • Enabled (Only required if you are using 2016/2019 AppStream instances)
  • Computer Configuration/Administrative Templates/System/App-V/Integration/Integration Root Global
    • %allusersprofile%\Microsoft\AppV\Client\Integration
  • Computer Configuration/Administrative Templates/System/App-V/Integration/Integration Root User
    • %userprofile%\Documents\Microsoft\AppV\Client\Integration
  • Computer Configuration/Administrative Templates/System/App-V/Publishing/Publishing Server 1 Settings
    • Publishing Server Display Name <Server Display Name>
    • Publishing Server URL http://<Server-FQDN>:Port#
    • Global Publishing Refresh False
    • Global Publishing Refresh On Logon False
    • Global Publishing Refresh Interval 0
    • Global Publishing Refresh Interval Unit Day
    • User Publishing Refresh True
    • User Publishing Refresh On Logon True
    • User Publishing Refresh Interval 1
    • User Publishing Refresh Interval Unit Day

Configuration of the AppStream 2.0 Image Builder

Before configuring the image builder, compile the required dynamic application framework and Apache Thrift dynamic link libraries (DLLs). For more information, see the Create a PowerShell-Based dynamic app provider in Amazon AppStream 2.0 post.

Installing the App-V client

When configuring a Windows Server 2012 R2 based AppStream image builder, you must install the App-V client manually. The App-V client is an integrated component of Server 2016 and 2019 OS, and does not require a manual App-V client install.

The installer for the App-V client can be found in your Microsoft Desktop Optimization Pack install media. You can use the following PowerShell commands to install the client as well as the latest service release for the client.

Start-Process appv_client_setup.exe -Wait -ArgumentList '/q /norestart /ACCEPTEULA /log C:\Windows\Logs\AppV51_Client_Install.log'
Start-Process AppV5.1RTM_Client_KB4074878.exe -Wait -ArgumentList '/q /norestart /ACCEPTEULA /log C:\Windows\Logs\AppV51_Client_HF02_Install.log'

You may have to perform a system restart after installing the client or its update packages.

PowerShell Script Configuration

Save the following two PowerShell scripts, as well as the dynamic application framework Client and Thrift DLLs in the folder C:\AppVSync.

The first script, appv-clientapps-usersync.ps1, checks your App-V application entitlements and creates a CSV with the required application data for adding them to the AppStream 2.0 application catalog.

On completion, it creates a custom Windows Event Log that triggers the appv-clientapps-dafupdate.ps1 script. That script imports the data from the CSV and, using the AppStream 2.0 dynamic application framework APIs, adds the application to the catalog.

The following is the appv-clientapps-usersync.ps1 script:

Import-Module AppvClient
[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing') | Out-Null

$logFile = "$home\Documents\Logs\AppStream\$env:username-$(get-date -f MM-dd-yyyy_HH_mm_ss)-appvsync.log"
New-Item -path $logfile -ItemType File -Force | Out-Null

Function Write-Log {
    Param ([string]$message)
    $stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
    $logoutput = "$stamp $message"
    Add-content $logfile -value $logoutput
}

Write-Log "Syncing with AppV Publishing Server..."
Sync-AppvPublishingServer $(Get-AppvPublishingServer).ID | Out-Null

Write-Log "Getting the user root path for the APPV VFS..."
$userrootpath = Get-ItemPropertyValue -Path HKLM:\SOFTWARE\Policies\Microsoft\AppV\Client\Integration -Name IntegrationRootUser

Write-Log "Getting list of AppV application packages available for user $env:USERNAME..."
$names = Get-AppvClientPackage -All | Where-Object -Property IsPublishedToUser -eq "True" | Select-Object -ExpandProperty Name

If($null -eq $names){
    Write-Log "There are no apps available to the user. Exiting..."
    Exit
}

$count = 0
$csvpath = "$home\AppStream\AppVSync\appv-progslist.csv"
$columns = '"Id","DisplayName","LaunchPath","IconData"'
$iconpath = "$env:temp\icons"
If (!(test-path -path $iconpath)) {
    Write-Log "Creating folder $iconpath..."
    New-Item -path $iconpath -ItemType Directory -Force | Out-Null
}

foreach ($name in $names) {
    Write-Log "Getting application display name, package ID and target path for package $name..."
    $app_pid = Get-AppvClientPackage -Name $name | Select-Object -ExpandProperty PackageId | Select-Object -ExpandProperty GUID
    $dname = (Get-AppvClientPackage -Name $name).getapplications() | Select-Object -ExpandProperty Name  
    $app_targetpath = (Get-AppvClientPackage -Name $name).getapplications() | Select-Object -ExpandProperty TargetPath
    
    Write-Log "Cleaning up target path for $dname..."
    If ($app_targetpath | Select-String -Pattern '[{ProgramFilesX86}]') {

        $app_targetpath = $app_targetpath.replace('[{ProgramFilesX86}]', 'ProgramFilesX86')

    }
    elseif ($app_targetpath | Select-String -Pattern "[{ProgramFiles}]") {

        $app_targetpath = $app_targetpath.replace('[{ProgramFiles}]', 'ProgramFiles')

    }

    Write-Log "Building the full path for application $dname..."
    $user_fullpath = [System.Environment]::ExpandEnvironmentVariables("$userrootpath\$app_pid\Root\VFS\$app_targetpath")

    Write-Log "Full path for $dname is $user_fullpath"

    if (Test-Path -path $user_fullpath) {
        
        Write-Log "$dname full path is valid..."

        if (Test-Path -Path $csvpath) {

            $count = (import-csv $csvpath | Measure-Object).Count

            if (Select-String -SimpleMatch -Pattern "$dname" -Path $csvpath) {
    
                Write-Log "$dname path already added to app list."
    
            }
            else {
                $count = $count + 1

                Write-Log "Getting $dname icon and converting it to Base64..."
                [System.Drawing.Icon]::ExtractAssociatedIcon($user_fullpath).ToBitmap().Save("$iconpath\$dname.png")
                $icondata = [convert]::ToBase64String((get-content "$iconpath\$dname.png" -Encoding byte))

                Write-Log "Adding $dname application infomration to list..."
                Add-Content -Path "$csvpath" -Value $("$($count),$($dname),$($user_fullpath),$($icondata)")
    
            }
        }
        else {
            $count = $count + 1

            Write-Log "Getting $name icon and converting it to Base64..."
            [System.Drawing.Icon]::ExtractAssociatedIcon($user_fullpath).ToBitmap().Save("$iconpath\$dname.png")
            $icondata = [convert]::ToBase64String((get-content "$iconpath\$dname.png" -Encoding byte))

            New-Item -path $csvpath -ItemType File -force | Out-Null
            Add-Content -Path "$csvpath" -Value $columns
            Write-Log "Adding $dname application infomration to list..."
            Add-Content -Path "$csvpath" -Value $("$($count),$($dname),$($user_fullpath),$($icondata)")
        }
    
    }
    else {
        
        Write-Log "$dname path is not valid..."
    }

}

Write-Log "Added $count application(s). Writing to event log..."
Write-EventLog -LogName AppVSync -source ClientRefresh -EntryType Information -eventID 25 -Message "Client Refresh for $env:username has completed successfully." -Verbose
Exit

The following is the appv-clientapps-dafupdate.ps1 script:

Add-Type -Path "C:\AppVSync\AS2DAF.dll"
Add-Type -Path "C:\AppVSync\Thrift.dll"
$currentuser = (get-wmiobject Win32_ComputerSystem).UserName.Split('\')[1]
$logfile = "C:\Users\$currentuser\Documents\Logs\AppStream\$currentuser-$(get-date -f MM-dd-yyyy_HH_mm_ss)-dafupdate.log"
New-Item -path $logfile -ItemType File -Force | Out-Null
Function Write-Log {
    Param ([string]$message)
    $stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
    $logoutput = "$stamp $message"
    Add-content $logfile -value $logoutput
}
$csvpath = "C:\Users\$currentuser\AppStream\AppVSync\appv-progslist.csv"
if (!(Test-Path -Path $csvpath)) {
    Write-Log "No application catalog update file. Exiting..."
    Exit
}
Write-Log "Opening connection to AS2 Thrift server..."
$transport = New-Object -TypeName Thrift.Transport.TNamedPipeClientTransport('D56C0258-2173-48D5-B0E6-1EC85AC67893')
$protocol = New-Object -TypeName Thrift.Protocol.TBinaryProtocol($transport)
$client = New-Object -TypeName AppStream.ApplicationCatalogService.Model.ApplicationCatalogService+Client($protocol)
$transport.open()
Write-Log "Connected..."
$usersid = (New-Object System.Security.Principal.NTAccount(($currentuser))).Translate([System.Security.Principal.SecurityIdentifier]).value
Write-Log "Getting applist for $currentuser, SID: $usersid..."
foreach ($app in (Import-Csv -path $csvpath)) {
    $appId = $app.Id
    $appdname = $app.DisplayName
    $apppath = $app.LaunchPath
    $appicon = $app.IconData
    Write-Log "Adding $appdname..."
    $applist = New-Object -TypeName AppStream.ApplicationCatalogService.Model.Application("$appId", "$appdname", "$apppath", "$appicon") 
    $getappreq = New-Object -TypeName Appstream.ApplicationCatalogService.Model.AddApplicationsRequest($usersid, $applist)
    $client.AddApplications($getappreq)
}
Write-Log "Closing connection..."
$transport.close()
Write-Log "Cleaning up catalog file..."
Remove-Item -Path "C:\Users\$currentuser\AppStream" -Recurse -Force
Exit

Creating the Windows Event Log and Source

Create a custom Windows Event Log and source for the appv-clientapps-usersync.ps1 script to trigger the appv-clientapps-dafupdate.ps1 script. As an administrator, use the following PowerShell command to create both:

New-EventLog -LogName "AppVSync" -Source "ClientRefresh"

Import Windows Scheduled Tasks

Next, import the following scheduled tasks into the Windows Task Scheduler:

  • App-V AppSync
  • App-V DAF Update

The first task, App-V AppSync, executes the script appv-clientapps-usersync.ps1 when you log in to the streaming instance. The second task, App-V DAF Update, executes when the appv-clientapps-usersync.ps1 script creates the custom event. The task executes the appv-clientapps-dafupdate.ps1 script. For more information about Windows Task Scheduler, see Task Scheduler on the Microsoft Windows Dev Center website.

You can use the following PowerShell commands to import the tasks, or do the process manually through the Task Scheduler console.

Register-ScheduledTask -xml (Get-Content 'App-V AppSync.xml' | Out-String) -TaskName "App-V AppSync"
Register-ScheduledTask -xml (Get-Content 'App-V DAF Update.xml' | Out-String) -TaskName "App-V DAF Update"

The following is theApp-V AppSync.xml file:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Triggers>
    <LogonTrigger>
      <Enabled>true</Enabled>
    </LogonTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <GroupId>S-1-5-32-545</GroupId>
      <RunLevel>LeastPrivilege</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>false</UseUnifiedSchedulingEngine>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
      <Arguments>-NoProfile –NonInteractive -ExecutionPolicy Bypass -Command "&amp; 'C:\AppVSync\appv-clientapps-usersync.ps1'"</Arguments>
    </Exec>
  </Actions>
</Task>

The following is the App-V DAF Update.xml file:

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="AppVSync"&gt;&lt;Select Path="AppVSync"&gt;*[System[Provider[@Name='ClientRefresh'] and EventID=25]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>S-1-5-18</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession>
    <UseUnifiedSchedulingEngine>false</UseUnifiedSchedulingEngine>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
      <Arguments>-NoProfile –NonInteractive -ExecutionPolicy Bypass -Command "&amp; 'C:\AppVSync\appv-clientapps-dafupdate.ps1'"</Arguments>
    </Exec>
  </Actions>
</Task>

Update the Agents.json config file

To enable dynamic app providers, update the Agents.json config file, %programdata%\Amazon\AppStream\AppCatalogHelper\DynamicAppCatalog.

The DisplayName parameter must match a Display Name an installed application’s registry key in HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall.

This can be for any application and does not have to be related to AWS or App-V.

For more information, see Enable and Test Dynamic App Providers in the AppStream 2.0 documentation.

Enable dynamic app providers in Image Assistant

Launch the AppStream 2.0 Image Assistant. If you configured Agents.json correctly, you should see the option to select Enable dynamic app providers under the Add Apps tab. Select it, then choose Next until you get to step 3. Test tab.

Testing integration functionality with a test user

To test the App-V integration functionality in your AppStream 2.0 image builder, use the switch user function and log in with an Active Directory user that has App-V application entitlements.

When you see the desktop, launch Image Assistant. After a few seconds, the App-V applications should appear. You can select one to launch it. If your image builder has specified locally installed applications, they also appear.

You can also check the log files located in %userprofile%\Documents\Logs\AppStream to verify that the correct apps have been added.

Create AppStream 2.0 image from configured and tested Image Builder

Before creating your AppStream 2.0 image, use the following PowerShell commands to clean up the App-V cache. Run these commands as an administrator.

net stop AppVClient
Get-AppvClientPackage -All | Remove-AppVClientPackage
Remove-Item C:\ProgramData\App-V\* -recurse -Force

After cleaning up the App-V cache, open Image Assistant, and optionally add any apps you would like to include with the image. Proceed with the normal image creation process. You will always see local applications specified in the image, and you can’t manage them with the dynamic application framework.

Conclusion

That’s it! You now have an AppStream 2.0 image, with dynamic apps enabled, configured, and tested to integrate with App-V. If you have App-V application entitlements, you can now see your applications when you land on the AppStream 2.0 catalog page.