PowerShell Intune Win32 Apps – Avoiding a busy MSI Installer

Scenario & Background:

Sometimes when you’re packaging apps to deploy via Intune, things need to be a little more complicated than just directly calling an MSI to install. You might need to install a series of MSIs or install an MSI and then copy files around. The easiest solution to make this all happen in one package (which will be the fastest solution from employee perspective too) is to use a PowerShell script. You can code the Script to move files around, run various installers in a certain order, etc, and then use it as the install command/media to call to for the package.

Where This Breaks Down:

A PowerShell script to install an MSI will work in a lot of situations without issue. Where it struggles is when things get busy on the machine. For instance, if you need to perform these actions during AutoPilot ESP, or at startup*, or shortly after the first logon post provisioning. The problem is that only one MSI can do anything (install, uninstall, modify, etc) at a time. During these times, the machine is often running through a lot of queued apps. While MSI’s plugged directly into Intune have collision avoidance with a busy MSI installer (I think), a PowerShell script will not. As such, it’s possible for some other action to still be in progress with the MSI Installer and your package then barf as it tries to run while the installer is busy.

*If you’re wondering how on earth to execute an Intune package at startup, that will be a talk for another time. Comments and likes will make this a priority topic.

A Real-Life Example:

I bumped into this problem during ESP which makes for a very good follow up to this article about troubleshooting app failures in ESP.

For a more expanded deep dive on this, I suggest you read the troubleshooting article, this article, and then my new article here: Autopilot ESP Bug: Office C2R Teams Installer Resulting in MSI Collision Nightmares – Getting the Most Out of Azure (azuretothemax.net)

I had a package which sometimes worked and sometimes was struggling to install. By pulling the application log (which is in the device diagnostics export) I was able to view logs from the MSI Installer and see that sometimes all/none of my MSI’s showed up in the log at all. What did show as running in the MSI logs at the same exact time my package should have been going according to the Intune Management Extension log was the Teams Machine wide Installer which runs after the office C2R completes.

It seems that this produces three scenarios.

  1. Devices are born with no Teams because the machine wide installer tried to kick off while another MSI was going. If pushed via a M365 apps for Win10 and later, Teams itself is not a required app so its failure does not stop ESP.
  2. Devices fail ESP because Teams is installing when a required app tries to go.
  3. Devices are fully successful because speed and luck lined up and both apps (Teams and the other MSI’s) managed to not run at the same time.

From this, I am not sure if the Teams Machine Wide installer has proper avoidance like other apps do. I noticed even direct MSI’s like Chrome run it over a few times. I think Teams is an unmonitored process to one degree or another that the office C2R install kicks off and hopes for the best.

So, How Can a PowerShell Script Avoid a Busy MSI Installer?

I fully expected this to be something I could Google, surely others had run into this problem before. While others had, I couldn’t find a nice and clean solution that really made sense in terms of how it worked. People knew how to check if the MSI installer was busy, but nobody seems to have put it into code that can easily be called and only proceed once it’s free. Running with the logic in this post, I made that idea into a reality with this Function.

Function CheckMSIStatus{
<#
.SYNOPSIS
Check the MSI installer status to see if it is busy. If busy, wait X time and check again. 
Only when free will this exit and then allow the script to proceed. Call this function right before running any MSI installer.

.NOTES
Author:      Maxton Allen
Contact:     @AzureToTheMax
Created:     2023-04-23
Updated:     
#>

do {
    try
    {
        $Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute");
        $Mutex.Dispose();
        Write-Warning "Warning: An installer is currently running."
        Add-Content "$($LogPathFull)" "$(get-date): Warning: MSI Installer is busy! Waiting $($MSIBusySleepTime) seconds!" -Force
        start-sleep -Seconds $MSIBusySleepTime
    }
    catch
    {
        Write-Host "An installer is not currently runnning."
        Add-Content "$($LogPathFull)" "$(get-date): MSI Installer is NOT busy! Proceeding." -Force
        $Mutex = $null
    }
} while ($null -ne $Mutex)
}
#endregion


This lovely bit of code will continuously loop and check to see if the MSI installer is busy, only exiting the loop once it is not. All you need to do is call this function right before your MSI installer goes. It will loop until the installer is free, and thus your command should process normally.

For example, here is a Firefox MSI install. I am rather fancy about these installers.

<#
.SYNOPSIS
Silent MSI app installer with collision avoidance for the MSI Installer service. 

.NOTES
Author:      Maxton Allen
Contact:     @AzureToTheMax
Created:     2023-04-23
Updated:     
#>


#Region Variables
#App Name
$AppName = "Firefox"
#Path where all log folders and files are stored
$LogFilePath = "C:\Windows\AzureToTheMax"
#Folder to create inside the log folder location
$LogFileFolderName = "Firefox"
#Name of Log File to store in the above log folder
$LogFileName = "FireFoxInstall.txt"
#Sleep time in seconds to wait if the MSI installer is busy before checking again
$MSIBusySleepTime = 30

$LogFilePathPartial = "$($LogFilePath)\$($LogFileFolderName)"
$LogPathFull = "$($LogFilePath)\$($LogFileFolderName)\$($LogFileName)"
#Endregion

#Region Functions
Function CheckMSIStatus{
<#
.SYNOPSIS
Check the MSI installer status to see if it is busy. If busy, wait X time and check again. 
Only when free will this exit and then allow the script to proceed. Call this function right before running any MSI installer.

.NOTES
Author:      Maxton Allen
Contact:     @AzureToTheMax
Created:     2023-04-23
Updated:     
#>

do {
    try
    {
        $Mutex = [System.Threading.Mutex]::OpenExisting("Global\_MSIExecute");
        $Mutex.Dispose();
        Write-Warning "Warning: An installer is currently running."
        Add-Content "$($LogPathFull)" "$(get-date): Warning: MSI Installer is busy! Waiting $($MSIBusySleepTime) seconds!" -Force
        start-sleep -Seconds $MSIBusySleepTime
    }
    catch
    {
        Write-Host "An installer is not currently runnning."
        Add-Content "$($LogPathFull)" "$(get-date): MSI Installer is NOT busy! Proceeding." -Force
        $Mutex = $null
    }
} while ($null -ne $Mutex)
}
#endregion

#Region script


#Create Storage Dirs as needed
if ((Test-Path $LogFilePath) -eq $false) {
New-Item $LogFilePath -ItemType Directory -ErrorAction SilentlyContinue > $null 
#Set dirs as hidden
$folder = Get-Item $LogFilePath
$folder.Attributes = 'Directory','Hidden' 
}
if ((Test-Path $LogFilePathPartial) -eq $false) {
New-Item $LogFilePathPartial -ItemType Directory -ErrorAction SilentlyContinue > $null 
$folder = Get-Item $LogFilePathPartial
$folder.Attributes = 'Directory','Hidden'
}


#Start log
Add-Content "$($LogPathFull)" "
$(get-date): $($AppName) installer starting on $($env:COMPUTERNAME)." -Force

#Check MSI status
CheckMSIStatus
#Run Install
Add-Content "$($LogPathFull)" "$(get-date): $($AppName) MSI install Beginning." -Force
Start-Process -FilePath ".\FireFoxSetup.msi" -ArgumentList "/qn" -Wait
Add-Content "$($LogPathFull)" "$(get-date): $($AppName) MSI install complete." -Force
Add-Content "$($LogPathFull)" "$(Get-Date): $($AppName) installer completing on $($env:COMPUTERNAME)." -Force
#endregion

exit 0



This will create a hidden folder “C:\Windows\AzureToTheMax” with another hidden folder inside named “Firefox.” Inside that folder will be a FireFoxInstall.txt with the logs of this installer. For instance, a normal install will read like…

04/23/2023 17:49:32: Firefox installer starting on Your-PC.
04/23/2023 17:49:32: MSI Installer is NOT busy! Proceeding.
04/23/2023 17:49:32: Firefox MSI install Beginning.
04/23/2023 17:49:37: Firefox MSI install complete.
04/23/2023 17:49:37: Firefox installer completing on Your-PC.

But, if the MSI installer is busy, it will read something like this.

04/23/2023 17:54:39: Firefox installer starting on Your-PC.
04/23/2023 17:54:39: Warning: MSI Installer is busy! Waiting 30 seconds!
04/23/2023 17:55:09: Warning: MSI Installer is busy! Waiting 30 seconds!
04/23/2023 17:55:39: Warning: MSI Installer is busy! Waiting 30 seconds!

04/23/2023 17:56:09: MSI Installer is NOT busy! Proceeding.
04/23/2023 17:56:09: Firefox MSI install Beggining.
04/23/2023 17:56:14: Firefox MSI install complete.
04/23/2023 17:56:14: Firefox installer completing on Your-PC.


How to Test This Script Yourself:

You can use whatever MSI’s you want really, assuming you have all the files and silent install commands, but here is an easy canned solution.

Grab an MSI of the latest Firefox here: Download the Firefox Browser in English (US) and more than 90 other languages (mozilla.org)
Get this next to the above PowerShell script and fill in the right name of the .MSI file.

Grab the latest MSI for Adobe Reader Enterprise: Adobe – Adobe Acrobat Reader DC Distribution
It’s an EXE, just extract it with 7-ZIP and locate the MSI inside.

Kick of the Reader MSI, it will take a minute to install. The moment it begins, try to run the PowerShell script. You should see warnings in the output that the MSI installer is busy. Go ahead and walk Reader through and complete it. As soon as it’s done and the last wait period runs out, FireFox should install.

The Bad News:

The problem still to be solved is Scenario A. While my package can now avoid Teams, Teams can’t seem to avoid my package still. So, I will likely be looking into breaking Teams out into its own installer as again, it just seems that the C2R process kicks it off whenever the bulk of office completed and does so without much monitoring or caution.

Conclusion:

You should now have a Function for easy use in your PowerShell scripts that allows you to easily avoid a busy MSI installer before running your MSI installer command. Happy packaging!


Disclaimer

The following is the disclaimer that applies to all scripts, functions, one-liners, setup examples, documentation, etc. This disclaimer supersedes any disclaimer included in any script, function, one-liner, article, post, etc.

You running this script/function or following the setup example(s) means you will not blame the author(s) if this breaks your stuff. This script/function/setup-example is provided AS IS without warranty of any kind. Author(s) disclaim all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall author(s) be held liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the script or documentation. Neither this script/function/example/documentation, nor any part of it other than those parts that are explicitly copied from others, may be republished without author(s) express written permission. Author(s) retain the right to alter this disclaimer at any time. 

It is entirely up to you and/or your business to understand and evaluate the full direct and indirect consequences of using one of these examples or following this documentation.

The latest version of this disclaimer can be found at: https://azuretothemax.net/disclaimer/

Leave a comment