Showing App Volumes Application Attach Progress to Users with PowerShell

Omnissa App Volumes is a great solution for separating applications from the base Windows image. Instead of installing every application directly into a golden image, applications can be packaged separately and dynamically attached to a virtual desktop when required.

This approach is especially useful in non-persistent Omnissa Horizon environments, where keeping the Windows image as clean and generic as possible simplifies image maintenance considerably. App Volumes packages can be assigned to users or groups and attached to the virtual desktop as part of the user logon process.

There is, however, one small user-experience problem.

After the Windows desktop appears, App Volumes might still be busy attaching applications.

From an administrator perspective this is completely normal. From an end-user perspective it can be confusing.

The Start menu is already available, Explorer is running, and the desktop looks ready for use. However, some applications might still be missing because their App Volumes packages have not finished attaching yet.

In this blog post, I will show how I used a PowerShell script to monitor the App Volumes attachment process directly from the Windows registry and display a small graphical progress window to the user.

The solution runs completely in user context, does not require administrative privileges, and does not depend on access to the App Volumes Agent log files.

The Use Case

The environment where I implemented this solution consists of Windows 11 non-persistent Omnissa Horizon virtual desktops with Omnissa App Volumes 2512.

Applications are dynamically provided through App Volumes instead of being installed directly in the Windows image.

During logon, Windows itself can become usable before App Volumes has completed processing all application packages.

This can result in a situation like this:

This means that simply detecting an attached VMDK or checking Get-Disk is not necessarily a good indication that the application is ready for the user.

What I wanted instead was something like this:

<INSERT PICTURE OF PROGRESS WINDOW – IN PROGRESS>

<INSERT PICTURE OF PROGRESS WINDOW – FINISHED>

The window automatically disappears after two seconds.

Using AppTracker to Determine the App Volumes Status

The main challenge was finding a reliable way to determine when App Volumes had actually finished attaching the applications.

Reading the App Volumes Agent log was not an ideal solution because the progress window must run completely in the logged-on user’s context. I also did not want to change permissions on App Volumes log files just to provide a progress indicator.

While investigating App Volumes 2512, I found a much more useful registry location:

HKLM:\SOFTWARE\Omnissa\AppVolumes\AppTracker

App Volumes creates subkeys underneath AppTracker for the applications it is processing.

For example:

HKLM └─ SOFTWARE └─ Omnissa └─ AppVolumes └─ AppTracker ├─ {0b91…} ├─ {19f2…} ├─ {3ea4…} ├─ {87c1…} └─ {f685…}

Each entry contains a Status value.

Monitoring these values during logon showed that the applications initially appeared with:

Status = 1

and changed individually to:

Status = 3

as App Volumes completed processing them.

Eventually every AppTracker entry had:

Status = 3

In one of my tests, 16 App Volumes entries were processed and the complete sequence took approximately 22 seconds.

This immediately gave me the information required for a progress indicator:

$Apps = @( Get-ChildItem 'HKLM:\SOFTWARE\Omnissa\AppVolumes\AppTracker' -ErrorAction SilentlyContinue ) $Ready = 0 $Pending = 0 foreach ($App in $Apps) { $Data = Get-ItemProperty -Path $App.PSPath -ErrorAction SilentlyContinue if ($Data.Status -eq 3) { $Ready++ } else { $Pending++ } }

The percentage can then simply be calculated using:

$Percentage = [math]::Round( ($Ready / $Apps.Count) * 100 )

So when 11 of 16 applications are ready, the graphical window can display:

11 of 16 ready 69%

Important: I have not found public Omnissa documentation that defines the numeric AppTracker\Status values. Status = 3 representing the completed state is therefore based on testing and observation with App Volumes 2512. I recommend validating this behavior again after upgrading the App Volumes Agent.

Adding a Small Safety Check

I did not want the script to immediately disappear as soon as the last currently known AppTracker entry reached status 3.

There could theoretically be a small timing window where another entry is created immediately afterwards.

For this reason, all entries must remain at status 3 for one second:

if ( $Apps.Count -gt 0 -and $Pending -eq 0 ) { if ($null -eq $ReadySince) { $ReadySince = Get-Date } if ( ((Get-Date) – $ReadySince).TotalMilliseconds -ge 1000 ) { break } } else { $ReadySince = $null }

If another pending application appears during that second, $ReadySince is reset and the script continues waiting.

Also notice the check:

$Apps.Count -gt 0

This is important because an empty AppTracker does not mean App Volumes has completed. It can simply mean that the script started before App Volumes populated the registry.

In that situation, the window displays:

Waiting for App Volumes…

Creating the Progress Window

With the App Volumes detection in place, the remaining part is mostly user experience.

The script uses Windows Forms, so no additional PowerShell modules are required:

Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing

The window is intentionally small and always on top:

$form = New-Object System.Windows.Forms.Form

$form.Size = New-Object System.Drawing.Size(240,300)

$form.FormBorderStyle = ‘None’

$form.BackColor = ‘White’

$form.TopMost = $true

$form.ShowInTaskbar = $false

I wanted it to appear in the bottom-right corner, similar to a notification, without overlapping the Windows taskbar.

The available Windows desktop area can be retrieved using:

$workingArea =

[System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea

The form is then positioned 10 pixels from the right edge and 10 pixels above the taskbar:

$form.StartPosition = 'Manual'

$form.Location = New-Object System.Drawing.Point(

($workingArea.Right - $form.Width - 10),

($workingArea.Bottom - $form.Height - 10)

)

The progress bar itself is driven directly by the AppTracker status:

$progressBar.Value = $Percentage

$counterLabel.Text =

"$Ready of $($Apps.Count) ready"

$percentageLabel.Text =

"$Percentage%"

There is no artificial timer controlling the progress bar. Its progress represents the number of App Volumes entries that have actually reached the completed state.

The Complete Script

Putting these pieces together results in the following PowerShell script:

$Path = 'HKLM:\SOFTWARE\Omnissa\AppVolumes\AppTracker'

Add-Type -AssemblyName System.Windows.Forms

Add-Type -AssemblyName System.Drawing

# ------------------------------------------------------------

# FORM

# ------------------------------------------------------------

$form = New-Object System.Windows.Forms.Form

$form.Size = New-Object System.Drawing.Size(240,300)

$form.FormBorderStyle = 'None'

$form.BackColor = 'White'

$form.TopMost = $true

$form.ShowInTaskbar = $false

# Bottom-right, 10 pixels above the taskbar

$workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea

$form.StartPosition = 'Manual'

$form.Location = New-Object System.Drawing.Point(

($workingArea.Right - $form.Width - 10),

($workingArea.Bottom - $form.Height - 10)

)

# ------------------------------------------------------------

# TITLE

# ------------------------------------------------------------

$title = New-Object System.Windows.Forms.Label

$title.Location = New-Object System.Drawing.Point(15,20)

$title.Size = New-Object System.Drawing.Size(210,30)

$title.Text = 'Loading applications'

$title.TextAlign = 'MiddleCenter'

$title.Font = New-Object System.Drawing.Font(

'Segoe UI',

12,

[System.Drawing.FontStyle]::Bold

)

$title.ForeColor = [System.Drawing.Color]::FromArgb(40,40,40)

$title.BackColor = 'White'

$form.Controls.Add($title)

# ------------------------------------------------------------

# STATUS TEXT

# ------------------------------------------------------------

$statusLabel = New-Object System.Windows.Forms.Label

$statusLabel.Location = New-Object System.Drawing.Point(15,70)

$statusLabel.Size = New-Object System.Drawing.Size(210,55)

$statusLabel.Text = "App Volumes applicationsnare being attached…”

$statusLabel.TextAlign = ‘MiddleCenter’

$statusLabel.Font = New-Object System.Drawing.Font(

‘Segoe UI’,

10

)

$statusLabel.ForeColor = [System.Drawing.Color]::FromArgb(70,70,70)

$statusLabel.BackColor = ‘White’

$form.Controls.Add($statusLabel)

# ————————————————————

# COUNTER

# ————————————————————

$counterLabel = New-Object System.Windows.Forms.Label

$counterLabel.Location = New-Object System.Drawing.Point(15,140)

$counterLabel.Size = New-Object System.Drawing.Size(210,40)

$counterLabel.Text = ‘Waiting for App Volumes…’

$counterLabel.TextAlign = ‘MiddleCenter’

$counterLabel.Font = New-Object System.Drawing.Font(

‘Segoe UI’,

11,

[System.Drawing.FontStyle]::Bold

)

$counterLabel.ForeColor = [System.Drawing.Color]::FromArgb(40,40,40)

$counterLabel.BackColor = ‘White’

$form.Controls.Add($counterLabel)

# ————————————————————

# PROGRESS BAR

# ————————————————————

$progressBar = New-Object System.Windows.Forms.ProgressBar

$progressBar.Location = New-Object System.Drawing.Point(20,200)

$progressBar.Size = New-Object System.Drawing.Size(200,20)

$progressBar.Minimum = 0

$progressBar.Maximum = 100

$progressBar.Value = 0

$progressBar.Style = ‘Continuous’

$form.Controls.Add($progressBar)

# ————————————————————

# PERCENTAGE

# ————————————————————

$percentageLabel = New-Object System.Windows.Forms.Label

$percentageLabel.Location = New-Object System.Drawing.Point(15,230)

$percentageLabel.Size = New-Object System.Drawing.Size(210,30)

$percentageLabel.Text = ‘0%’

$percentageLabel.TextAlign = ‘MiddleCenter’

$percentageLabel.Font = New-Object System.Drawing.Font(

‘Segoe UI’,

10

)

$percentageLabel.ForeColor = [System.Drawing.Color]::FromArgb(90,90,90)

$percentageLabel.BackColor = ‘White’

$form.Controls.Add($percentageLabel)

# ————————————————————

# DISPLAY WINDOW

# ————————————————————

$form.Show()

[System.Windows.Forms.Application]::DoEvents()

# ————————————————————

# APP VOLUMES MONITORING

# ————————————————————

$ReadySince = $null

while ($true) {

 

$Apps = @(

Get-ChildItem $Path -ErrorAction SilentlyContinue

)

 

$Ready   = 0

$Pending = 0

 

foreach ($App in $Apps) {

 

$Data = Get-ItemProperty

-Path $App.PSPath

-ErrorAction SilentlyContinue

 

if ($Data.Status -eq 3) {

$Ready++

}

else {

$Pending++

}

}

 

# ——————————————————–

# UPDATE GUI

# ——————————————————–

 

if ($Apps.Count -gt 0) {

 

$Percentage = [math]::Round(

($Ready / $Apps.Count) * 100

)

 

if ($Percentage -gt 100) {

$Percentage = 100

}

 

$progressBar.Value = $Percentage

 

$counterLabel.Text =

“$Ready of $($Apps.Count) ready”

 

$percentageLabel.Text =

“$Percentage%”

 

$statusLabel.Text =

“App Volumes applicationsnare being attached..."

}

else {

 

$counterLabel.Text =

'Waiting for App Volumes...'

 

$percentageLabel.Text = '0%'

$progressBar.Value = 0

}

 

[System.Windows.Forms.Application]::DoEvents()

 

# --------------------------------------------------------

# DETECT ALL APPLICATIONS READY

# --------------------------------------------------------

 

if (

$Apps.Count -gt 0 -and

$Pending -eq 0

) {

 

if ($null -eq $ReadySince) {

$ReadySince = Get-Date

}

 

# All applications must remain Status 3 for 1 second

if (

((Get-Date) - $ReadySince).TotalMilliseconds -ge 1000

) {

break

}

}

else {

$ReadySince = $null

}

 

Start-Sleep -Milliseconds 100

}

 

# ------------------------------------------------------------

# COMPLETED

# ------------------------------------------------------------

 

$progressBar.Value = 100

 

$title.Text = 'Applications attached'

 

$statusLabel.Text =

"All applications havenbeen attached.”

 

$counterLabel.Text =

“$Ready of $($Apps.Count) ready”

 

$percentageLabel.Text = ‘100%’

 

[System.Windows.Forms.Application]::DoEvents()

 

# Keep success message visible for 2 seconds

$timer = [System.Diagnostics.Stopwatch]::StartNew()

 

while ($timer.Elapsed.TotalSeconds -lt 2) {

 

[System.Windows.Forms.Application]::DoEvents()

Start-Sleep -Milliseconds 50

}

 

$form.Close()

$form.Dispose()

Running It During User Logon

For this use case, the script should be started as part of the interactive user logon process.

In an Omnissa Horizon environment, Dynamic Environment Manager is an obvious place to launch it, but a user logon script or scheduled task can also be used.

I recommend starting PowerShell with the console hidden:

powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File “AppVolumesProgress.ps1”

The script itself does not require elevation. It only reads the AppTracker information from HKLM and displays the Windows Forms interface in the user’s session.

Why Not Just Wait 30 Seconds?

The alternative would be very simple:

Start-Sleep -Seconds 30

But that does not tell us anything about the actual state of App Volumes.

If the packages finish after 12 seconds, the user unnecessarily waits another 18 seconds.

If they take 40 seconds, the message disappears 10 seconds too early.

The AppTracker approach instead waits for the actual observed state:

Windows desktop

|

v

App Volumes starts processing

|

+– 3 of 16 ready

|

+– 7 of 16 ready

|

+– 11 of 16 ready

|

+– 15 of 16 ready

|

+– 16 of 16 ready

|

v

Status remains stable for 1 second

|

v

Applications attached

|

v

Progress window closes

This also means that the solution automatically adapts to users with different application assignments.

Conclusion

In a non-persistent Horizon desktop, Windows being ready does not necessarily mean that all dynamically delivered applications are ready.

That distinction can be confusing to users. The Start menu is available and the desktop looks usable, but App Volumes can still be attaching applications in the background.

By monitoring:

HKLM\SOFTWARE\Omnissa\AppVolumes\AppTracker

the PowerShell script can provide useful feedback about what is actually happening instead of displaying an arbitrary timer.

The result is a lightweight solution that:

runs completely in user context;

requires no administrative privileges;

does not require access to the App Volumes Agent logs;

shows actual attachment progress;

supports different application assignments per user;

and automatically disappears when the applications are ready.

One final consideration is that AppTracker and the meaning of Status = 3 should be regarded as implementation details observed with App Volumes 2512, rather than a documented automation interface. I therefore recommend validating the behavior again after future App Volumes Agent upgrades.

For this particular use case, however, it provides exactly the information needed to make the final part of a non-persistent VDI logon much more transparent to the user.

You may also like...