Create Custom Programs and Features Entries (ARP Entries)

So you’ve created a PSADT package that contains many customizations or multiple MSI and/or Setup.exe that must be uninstalled in a specific order.
It install/uninstall from SCCM fine but if somebody installs the the apps from Programs and Features, the uninstall will be incomplete or prevent a reinstall.
What if you could create your own entry in Programs and Features and hide the real one(s)?

Or,
what if you wanted to create a PSADT script without MSIs or setup.EXE but still wanted it to show in Programs and Features to uninstall it locally?

BTW: ARP = Add/Remove Programs = Programs and Features = Apps & Features

The changes you need to do:

  1. add 3 functions to your AppDeployToolkitExtensions.ps1 file (posted in a reply due to the 32000 char limit of Discourse)
	Set-LocalDiskUninstall
	Set-ARPChildOfParent
	Set-CustomARP
  1. Add this to AppDeployToolkitExtensions.ps1 file:
	##*===============================================
	##* SCRIPT BODY
	##*===============================================
	#Local cache location of PSADT file --> NO SPACES in the path!!!
	[String]$configLocalUninstallCache = "C:\PSADT\uninstall"
  1. Rename the Deploy-Application.ps1
    As long as it’s different, more than one PSADT can use this method

  2. add to following changes to your Deploy-Application.ps1 :

	##*===============================================
	##* VARIABLE DECLARATION
	##*===============================================
	...
	## Variables: Install Titles (Only set here to override defaults set by the toolkit)
	
	[string]$installName = ''
	[string]$installTitle = 'Custom ARP Example'	#used by Custom ARP, PSADT GUI prompts, PSADT balloonTip/toasts
	[string]$installName		= ($MyInvocation.MyCommand.Name).Replace('.ps1','')  # e.g. 'used by Custom ARP, localDiskUninstall, etc.
	[string]$AppMSIName			= 'Template_Test_v1r1.MSI' # actual name of the MSI in the \Files folder
    [string]$AppMSICode			= '{ABCDEF00-0000-0000-0000-000000000000}' # MSI Product Code of MSI above (used for removal)
		##*===============================================
		##* INSTALLATION
		##*===============================================
		[string]$installPhase = 'Installation'
		Set-LocalDiskUninstall	#Needed for Custom ARP entry to work

		Execute-MSI -Action Install -Path $AppMSIName -ContinueOnError $False -LogName "${AppMSIName}_MSI"
		Set-ARPChildOfParent -ArpKeyName $AppMSICode #Hide ARP key created by MSI
		#This should be placed near the **end** of the INSTALLATION section
		#Set-CustomARP uses \SupportFiles\$InstallName.ico if possible, otherwise uses PSADT icon (AppDeployToolkitLogo.ico) for Custom ARP entry
		Set-CustomARP
		##*===============================================
		##* POST-INSTALLATION
		##*===============================================
		##*===============================================
		##* UNINSTALLATION
		##*===============================================
		[string]$installPhase = 'Uninstallation'
		
		Set-ARPChildOfParent -ArpKeyName $AppMSICode -Remove #UN-Hide ARP entry created by MSI or EXE *BEFORE* uninstalling
		Execute-MSI -Action Uninstall -Path $AppMSICode -ContinueOnError $False -LogName "${AppMSIName}_MSI"
		#This should be placed near the **end** of the UNINSTALLATION section
		Set-CustomARP -PkgName $InstallName -Remove
		Set-LocalDiskUninstall -PkgName $InstallName -Remove
		##*===============================================
		##* POST-UNINSTALLATION
		##*===============================================
2 Likes

Code listing of the 3 functions:

#region Function Set-CustomARP
Function Set-CustomARP {
<#
.SYNOPSIS
	Creates/removes custom ARP entries
	This is used to hide multiple ARP entries under ONE custom ARP entry
	Makes uninstalling complex packages uninstall using a PSADT script
	Makes it possible to get a log file when uninstalling from ARP
	Makes ARP/Program and Features/ cleaner, easier to understand
.DESCRIPTION
	Creates/removes custom ARP entries to make it possible to get a log file when uninstalling from ARP
	You can force it to be 32Bit, 64Bit, but defaults to Auto 
	'Auto' makes it search for its "children" in both by looking for "ArpKeyName" values in other ARP entries's ParentKeyName value (using Set-ARPChildOfParent)
		If no children are found, the Custom ARP entry will be 64bit.
		If both are found, the Custom ARP entry will be 64bit.
	Set-CustomARP must be called *AFTER*:
		-Set-LocalDiskUninstall function (Usually before installing packages)
		-ALL packages (MSI/EXE/Etc.) are installed
		-Set-ARPChildOfParent function is called to hide ARP entry of each (MSI/EXE/Etc.)
	NOTE: Set-ARPChildOfParent function will create ParentKeyName values that this function looks for.
	NOTE: ARP = Add/Remove Programs = Programs and Features = Apps & Features
.PARAMETER ArpKeyName
	Name of Registry Key of the NEW ARP entry, Defaults to $installName (PSADT var)
	Has Alias of PkgName
.PARAMETER ArpDisplayName
	Name that will be shown in ARP/Programs&Features entry, Defaults to $installTitle (PSADT var)
.PARAMETER ArpContact
	Contact that will be shown in ARP/Prog&Features entry, Defaults to $appScriptAuthor (PSADT var)
.PARAMETER ArpPublisher
	Publisher that will be shown in ARP/Prog&Features entry, Defaults to $ArpContact (mentioned above)
.PARAMETER ArpDisplayIcon
	Full Path to the Icon that will be shown in ARP/Prog&Features entry, 
	Defaults to \SupportFiles\$InstallName.ico in Local uninstall Cache
	If no match occurs, defaults to \AppDeployToolkit\AppDeployToolkitLogo.ico in Local uninstall Cache
	ArpDisplayIcon should be able to be pointed to an .ico or an .exe. 
.PARAMETER ArpDisplayVersion
	Version number that will be shown in ARP/Prog&Features entry, Defaults to $appVersion (PSADT var)
.PARAMETER ArpInstallDate
	Date of Installation that will be shown in ARP/Prog&Features entry, Defaults to an automatically generated date [Rarely used]
.PARAMETER ArpUninstallString
	UnInstallation command that will be triggered by this custom ARP/Prog&Features entry, Defaults to uninstalling this $installName.ps1 from Local uninstall Cache
	CAVEATS:
	-UninstallString does not support file association so you must specify the Exe
	-Windows7-Progs&Feature (aka ARP) is 32bit, we specified a full path to PowerShell.exe
	-Windows10-Progs&Feature (aka ARP) is 64bit but Win10 also has APPs&Feature where specifying a full path to the EXE will make it fail to launch.
.PARAMETER ArpEstimatedSize
	Estimated Size of installed application will be shown in ARP/Prog&Features entry. 
	Uses size of files in \Files folder. If none, defaults to a bogus value.
	This is required or else ARP/Prog&Features takes longer to display while Windows 
	goes on a scavenger hunt for the size and wastes your time as Prog&Features loads!
.PARAMETER BitNess
	Sets the bitness of the ARP entry - Optional [Rarely used]
	Can be forced to 32Bit,64Bit or Auto. Defaults to 'Auto'.
.PARAMETER Remove
	Removes the custom ARP entry
.PARAMETER ExistTest
	Returns $true or $false if $ArpKeyName exist in ARP region of the registry. 32 or 64 bit [Rarely used]
.PARAMETER ContinueOnError
    Continue if an error is encountered. Default is: $false.
.EXAMPLE	
	Set-CustomARP
	Creates ARP Entry using $InstallName.ico or AppDeployToolkitLogo.ico in local Uninstall folder ($configLocalUninstallCache)
.EXAMPLE
	Set-CustomARP -ArpDisplayIcon "C:\Program Files\1E\Agent\WakeUp\WakeUpAgt.exe"
	Creates ARP Entry and uses the first icon in WakeUpAgt.exe
.EXAMPLE
	Set-CustomARP -ContinueOnError $false
.EXAMPLE
	Set-CustomARP -PkgName $installName -Remove
	Remove the custom ARP entry
.EXAMPLE
	Set-CustomARP -ArpKeyName $installName -Remove
	Remove the custom ARP entry
.EXAMPLE
	Set-CustomARP -ExistTest
	Tests if ARP entry for $installName exists. 
.NOTES
	Author: Denis St-Pierre (Ottawa, Canada)
	-Tested on Windows 10 and Windows 7
	Base on concepts by Todd MacNaught (Ottawa, Canada)
	Depends on Set-LocalDiskUninstall and Set-ARPChildOfParent functions
	Uses $configLocalUninstallCache to point to an existing folder e.g. c:\AdmUtils\uninstall
#>
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$false,HelpMessage="Registry key name for ARP Entry - Optional")]
        [ValidateNotNullorEmpty()]
		[Alias('PkgName')]
        [string]$ArpKeyName = $installName,
		[Parameter(Mandatory=$false,HelpMessage="DisplayName for ARP Entry - Optional")]
        [ValidateNotNullorEmpty()]
		[String]$ArpDisplayName = $installTitle,
		[Parameter(Mandatory=$false,HelpMessage="Contact for ARP Entry - Optional")]
		[ValidateNotNullOrEmpty()]
		[String]$ArpContact = $appScriptAuthor,
		[Parameter(Mandatory=$false,HelpMessage="Publisher for ARP Entry - Optional")]
        [ValidateNotNullorEmpty()]
		[String]$ArpPublisher = $appVendor,
		[Parameter(Mandatory=$false,HelpMessage="Icon for ARP Entry (Path on Target or Deploy-Application_v1r1.ico) - Optional")]
        [ValidateNotNullorEmpty()]
		[String]$ArpDisplayIcon = $(If (Test-Path "$configLocalUninstallCache\$InstallName\SupportFiles\$InstallName.ico") {"$configLocalUninstallCache\$InstallName\SupportFiles\$InstallName.ico" } else {"$configLocalUninstallCache\${ArpKeyName}\AppDeployToolkit\AppDeployToolkitLogo.ico"}),
		[Parameter(Mandatory=$false,HelpMessage="Version number for ARP Entry - Optional")]
        [ValidateNotNullorEmpty()]
		[String]$ArpDisplayVersion = $appVersion,
		[Parameter(Mandatory=$false,HelpMessage="Date of Installation for ARP Entry - Optional")]
        [ValidateNotNullorEmpty()]
		[string]$ArpInstallDate = ((Get-Date -Format 'yyyyMMdd').ToString()), # Ex: 20150720
		[Parameter(Mandatory=$false,HelpMessage="UnInstallation command for ARP Entry - Optional")]
        [ValidateNotNullorEmpty()]
		[string]$ArpUninstallString = "PowerShell -executionpolicy bypass `"$configLocalUninstallCache\$ArpKeyName\${installName}.ps1`" Uninstall NonInteractive",
		[Parameter(Mandatory=$false,HelpMessage="EstimatedSize of installed application for ARP Entry - Optional")]
		[ValidateNotNullOrEmpty()]
		[String]$ArpEstimatedSize,
		[Parameter(Mandatory=$false,HelpMessage="Sets the bitness of the ARP entry - Optional")]
		[ValidateSet('32Bit','64Bit','Auto')]
		[String]$BitNess = 'Auto',
		[Parameter(Mandatory=$false,HelpMessage="Removes custom ARP entry")]
		[ValidateNotNullorEmpty()]
		[Switch]$Remove = $false,
		[Parameter(Mandatory=$false,HelpMessage='Tests for custom ARP entries, returns $true or $false.')]
		[ValidateNotNullorEmpty()]
		[Switch]$ExistTest = $false,
		[Parameter(Mandatory=$false)]
		[boolean]$ContinueOnError = $false
    )
    Begin {
        ## Get the name of this function and write header
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
    }	
	Process {
		Try {
				Try {
					#Can't use PSADT's [Array]$regKeyApplications because we need to know if it's 32 or 64 bit.
					[string]$HKLMUninstallKey64bit	= 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
					[string]$HKLMUninstallKey32bit	= 'HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
					
					If ($ExistTest) {
						If ($ArpKeyName -match ":\\") {
							Write-Log "`$ArpKeyName [$ArpKeyName] contains illegal characters ':\' " -Source ${CmdletName} -Severity 3
							Throw "`$ArpKeyName contains illegal characters ':\' "
						}
						$ArpFound = $false
						ForEach ($regKey in $regKeyApplications) {	#$regKeyApplications is from PSADT. Table holds 32 and 64 bit ARP locations
							[string]$_TARGETPKGUNINSTALLKEY = "$regKey\$ArpKeyName"
							Write-Log "Looking for [$_TARGETPKGUNINSTALLKEY] to TEST for ARP entry ..." -Source ${CmdletName}
							If (Test-Path $_TARGETPKGUNINSTALLKEY) {
								Write-Log "Found [$ArpKeyName] ARP entry" -Source ${CmdletName}
								$ArpFound = $true
							}
						}
						Write-Output $ArpFound
					} ElseIf ($Remove) { #Remove ARP entry
						[Int32]$numOfRemovedArpEntries = 0
						ForEach ($regKey in $regKeyApplications) {	#$regKeyApplications is from PSADT. Table holds 32 and 64 bit ARP locations
							[string]$_TARGETPKGUNINSTALLKEY = "$regKey\$ArpKeyName"
							Write-Log "Looking for [$_TARGETPKGUNINSTALLKEY] to remove ARP entry ..." -Source ${CmdletName}
							If (Test-Path $_TARGETPKGUNINSTALLKEY) {
								Write-Log "Removing [$ArpKeyName] ARP entry" -Source ${CmdletName}
								Remove-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -ContinueOnError $ContinueOnError
								$numOfRemovedArpEntries++ #CAVEAT: Don't Cast and increment at the same time or it will output the value
							}
						}
						If ( $numOfRemovedArpEntries -lt 1 ) {
							Write-Log "ARP Entry [$ArpKeyName] does not exist. Nothing to Delete." -Severity 2 -Source ${CmdletName}
						}
					} Else { 		#Create ARP entry
						Write-log "Using [$ArpDisplayIcon] for ARP icon" -Source ${CmdletName}
					
						#Determine BitNess of Arp entry
						If ($BitNess -eq 'Auto') {
							Write-Log "Looking for ARP Children with ParentKeyName set to [$ArpKeyName]..." -Source ${CmdletName}
							Remove-Variable AllArpKeys,ArpKey,Values -ErrorAction SilentlyContinue
							[System.Array]$AllArpKeys = Get-ChildItem -Path $HKLMUninstallKey32bit -ErrorAction Stop
							ForEach ($ArpKey in $AllArpKeys) {
								[PSObject]$Values = $ArpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}
								If ( $Values.ParentKeyName -eq $ArpKeyName) {
									[String]$BitNess = '32Bit'
									Write-Log "Found 32bit Child [$($Values.DisplayName)]	Keyname:[$($ArpKey.PSChildName)]" -Source ${CmdletName}
								}
							}
							Remove-Variable AllArpKeys,ArpKey,Values -ErrorAction SilentlyContinue
							[System.Array]$AllArpKeys = Get-ChildItem -Path $HKLMUninstallKey64bit -ErrorAction Stop
							ForEach ($ArpKey in $AllArpKeys) {
								[PSObject]$Values = $ArpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}
								If ( $Values.ParentKeyName -eq $ArpKeyName) {
									[String]$BitNess = '64Bit'
									Write-Log "Found 64bit Child [$($Values.DisplayName)]	Keyname:[$($ArpKey.PSChildName)]" -Source ${CmdletName}
								}
							}
							If ($BitNess -eq $null) {
								[String]$BitNess = '64Bit'
								Write-Log "No Children ARP entries found. Defaulting to [$BitNess]"  -Source ${CmdletName} -Severity 2
							}						
						}
						If ($BitNess -eq '32Bit') {
							[string]$_TARGETPKGUNINSTALLKEY = "$HKLMUninstallKey32bit\${ArpKeyName}"
						} Else {
							[string]$_TARGETPKGUNINSTALLKEY = "$HKLMUninstallKey64bit\${ArpKeyName}"
						}

						Write-Log "Creating [$BitNess] ARP entry for [${ArpKeyName}]" -Source ${CmdletName}
						Write-Log "Variable `$_TARGETPKGUNINSTALLKEY resolved to [$_TARGETPKGUNINSTALLKEY]."  -Source ${CmdletName}
						Write-Log "Variable `$ArpUninstallString resolved to [$ArpUninstallString]."  -Source ${CmdletName}
						Write-Log "Variable `$ArpInstallDate resolved to [$ArpInstallDate]."  -Source ${CmdletName}
						Write-Log "Variable `$ArpDisplayIcon resolved to [$ArpDisplayIcon]."  -Source ${CmdletName}

						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "Contact" -Value $ArpContact -Type String -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "DisplayIcon" -Value $ArpDisplayIcon  -Type String -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "DisplayName" -Value $ArpDisplayName -Type String -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "DisplayVersion" -Value $ArpDisplayVersion  -Type String -ContinueOnError $ContinueOnError
						
						If  ($ArpEstimatedSize) {
							Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "EstimatedSize" -Value $ArpEstimatedSize -Type DWord -ContinueOnError $ContinueOnError
						} else { #NOTE: if EstimatedSize is not set, ARP/Programs and Features goes on a scavenger hunt for the size and wastes time!
							Try {
								#  Determine the size of the \Files\ folder
								$colItems = (Get-ChildItem $dirFiles -recurse | Measure-Object -property length -sum) 
								[int32]$ArpEstimatedSize = ($colItems.sum / 1KB)
								If ($ArpEstimatedSize -lt 1) {
									#  Determine the size of the \SupportFiles\ folder
									$colItems = (Get-ChildItem $dirSupportFiles -recurse | Measure-Object -property length -sum) 
									[int32]$ArpEstimatedSize = ($colItems.sum / 1KB)
								}
							}
							Catch {
								Write-Log -Message "Failed to calculate disk space requirement from source files (will use bogus value). `r`n$(Resolve-Error)" -Severity 2 -Source ${CmdletName}
								[int32]$ArpEstimatedSize = 6835
							}
							Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "EstimatedSize" -Value $ArpEstimatedSize -Type DWord -ContinueOnError $ContinueOnError
						}

						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "InstallDate" -Value $ArpInstallDate -Type String -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "NoRemove" -Value 0 -Type DWord -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "NoRepair" -Value 1 -Type DWord -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "NoModify" -Value 1 -Type DWord -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "Publisher" -Value $ArpPublisher -Type String -ContinueOnError $ContinueOnError
						Set-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "UninstallString" -Value $ArpUninstallString -Type ExpandString -ContinueOnError $ContinueOnError
						If (Test-RegistryValue -Key $_TARGETPKGUNINSTALLKEY -Value 'WindowsInstaller') { #Because Remove-RegistryKey whines in red if not exist
							Remove-RegistryKey -Key $_TARGETPKGUNINSTALLKEY -Name "WindowsInstaller" -ContinueOnError $true
						}
					}
				} Catch {
					$Verb = 'create'
					If ($Remove) { $Verb = 'remove' }
					If ($ExistTest) { $Verb = 'Test' }
					Write-Log -Message "Failed to $Verb Custom ARP enter[$ArpKeyName]. `r`n$(Resolve-Error)" -Severity 3 -Source ${CmdletName}
					If (-not $ContinueOnError) {
						Throw "Failed to $Verb Custom ARP enter[$ArpKeyName]: $($_.Exception.Message)"
					}
				}
		} Catch {
			[string]$ErrorMessage = "$($_.Exception.Message) $($_.ScriptStackTrace) $($_.Exception.InnerException)"
			Write-Log $ErrorMessage -Severity 3 -Source ${CmdletName}
		}
	}
    End {
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
    }
}
#endregion Function Set-CustomARP


#region Function Set-ARPChildOfParent
Function Set-ARPChildOfParent {
<#
.SYNOPSIS
	Creates/removes ParentKeyName and ParentDisplayName values in ARP region of registry for a given $ArpKeyName
	These values are also used as flags so that Set-CustomARP will show the as Children of the Custom ARP entry
.DESCRIPTION
	Creates/removes ParentKeyName and ParentDisplayName values in ARP region of registry for a given $ArpKeyName. 
	This function must be run *AFTER* *EACH* Execute-MSI or Execute-Process in the case of setup.exe
	Sets WindowsInstaller=0 even if the ARP entry is from an MSI (Or else the ARP entry does not become a child entry)
	Will also set SystemComponent=0 so that this child entry is not hidden in ARP's "View Installed Updates'
		
	The Set-CustomARP function must be run *AFTER* this function. 
	Set-CustomARP will scan 32bit and 64bit ARP regions of registry for these Parent* values
	and will create its ARP entry to match the bitness of its children.
	NOTE: ARP = Add/Remove Programs = Programs and Features = Apps & Features
	We call it ARP because this idea started with Windows XP
.PARAMETER ArpKeyName
	Name of Registry Key of the ARP entry to make a Child of $installName (by default)
	This key must exist PRIOR to calling this function (this is why you install the app first)
.PARAMETER Remove
	Removes the ParentKeyName and ParentDisplayName values that makes the ARP entry a Child of $installName [by default]
.PARAMETER ParentKeyName
	Keyname of the ARP entry that will be created with Set-CustomARP. Defaults to $installName [Rarely used and highly optional]
.PARAMETER ParentDisplayName
	Text that will show in ARP that will be created with Set-CustomARP. Defaults to $installTitle [Rarely used and highly optional]
.PARAMETER IgnoreMissingUninstallString
	By Default, if there is no UninstallString we do not set ParentKeyName because it caused weird issues 
	If you still want to set ParentKeyName for the ARP entry in spite of this, set this parameter to $true
	Rarely used
.PARAMETER OldParentKeyName
	Replaces the contents of ParentKeyName and ParentDisplayName values in existing ARP entries [Rarely used]
	This used when you are releasing a new version of an already deployed package that you do not want to uninstall/reinstall for minor changes.
	This became a need to prevent reinstalling a 3GB package over the VPN for Thousands of people during COVID.
.PARAMETER ContinueOnError
	Continue if unable to set or remove values or keys. Default is: $false.
	Should default to $true if a removal to prevent failed uninstalls (TODO: try to set in parameters in FUTURE if possible)
.EXAMPLE
	Set-ARPChildOfParent -ArpKeyName $MSIGUID 
.EXAMPLE
	Adds ParentKeyName and ParentDisplayName for -ArpKeyName $installName
	CAVEAT:You must do this AFTER you install the package that created this ARP Entry.
	Set-ARPChildOfParent -ArpKeyName $installName
.EXAMPLE
	Removes ParentKeyName and ParentDisplayName for -ArpKeyName $installName
	CAVEAT:You must do this BEFORE you uninstall the package that created this ARP Entry. Otherwise, uninstall may fail or leave junk in the registry.
	Set-ARPChildOfParent -ArpKeyName $installName -Remove
.EXAMPLE
	Replaces the contents of ParentKeyName and ParentDisplayName values in existing ARP entries where ParentKeyName is currently set to 'MicrosoftOfficeProPlus_V365R1'
	Set-ARPChildOfParent -OldParentKeyName 'MicrosoftOfficeProPlus_V365R1'
.NOTES
	Author: Denis St-Pierre (Ottawa, Canada)
	-Tested on Windows 10 and Windows 7	
	ARP = Add/Remove Programs (Now known as Programs and Features in Windows7 and Apps & Features in Windows10)
#>
    [CmdletBinding()]
    Param (
	    [Parameter(Mandatory=$true,ParameterSetName='Set',HelpMessage="name of ARP Registry key to become a Child of another ARP entry")]
        [Parameter(Mandatory=$true,ParameterSetName='Remove',HelpMessage="name of ARP Registry key to become a Child of another ARP entry")]
		[Parameter(Mandatory=$false,ParameterSetName='RenameParentKeyName')]
		[ValidateNotNullorEmpty()]
		[string]$ArpKeyName,
		[Parameter(Mandatory=$true,ParameterSetName='Remove',HelpMessage="Removes ParentKeyName and ParentDisplayName values - Optional")]
		[ValidateNotNullorEmpty()]
		[Switch]$Remove = $false,
		[Parameter(Mandatory=$false,HelpMessage="Removes the Parent* values that makes the ARP entry a Child of `$installName [by default]")]
		[ValidateNotNullOrEmpty()]
		[String]$ParentKeyName = $installName,
		[Parameter(Mandatory=$false)]
		[ValidateNotNullOrEmpty()]
		[String]$ParentDisplayName = $installTitle,
		[Parameter(Mandatory=$false)]
		[boolean]$IgnoreMissingUninstallString=$False,
		[Parameter(Mandatory=$false,ParameterSetName='Set')]
		[Parameter(Mandatory=$false,ParameterSetName='Remove')]
		[Parameter(Mandatory=$true,ParameterSetName='RenameParentKeyName')]
		[String]$OldParentKeyName,
		[Parameter(Mandatory=$false)]
		[boolean]$ContinueOnError = $false
    )
    Begin {
        ## Get the name of this function and write header
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header

		[string]$HKLMUninstallKey64bit	= 'HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
		[string]$HKLMUninstallKey32bit	= 'HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
	}	
	Process {
		Try {
				[Bool]$FoundArpKey = $false
				ForEach ($Key in @($HKLMUninstallKey64bit,$HKLMUninstallKey32bit) ) {	#Check in both 32bit and 64bit ARPs
					If (Test-Path -Path "$Key\$ArpKeyName" -PathType Container) {
						If ($Remove) {
							Write-log "Removing Parent values in [$ArpKeyName]..." -Source ${CmdletName}
							Remove-RegistryKey -Key "$Key\$ArpKeyName" -Name 'ParentKeyName' -ContinueOnError $true
							Remove-RegistryKey -Key "$Key\$ArpKeyName" -Name 'ParentDisplayName' -ContinueOnError $true
							Remove-RegistryKey -Key "$Key\$ArpKeyName" -Name 'SystemComponent' -ContinueOnError $true
							#TBD: do we need WindowsInstaller =1 if entry is for an MSI? No but we must remove the value or else the Reg key will stay
							Remove-RegistryKey -Key "$Key\$ArpKeyName" -Name 'WindowsInstaller' -ContinueOnError $true
							[Bool]$FoundArpKey = $true
						} ElseIf ($OldParentKeyName) {
							Write-log "Replacing ALL Parent values currently set to [$OldParentKeyName]..." -Source ${CmdletName}
							Write-log "IOW: [$ParentKeyName] is stealing all of [$OldParentKeyName]'s Children in ARP" -Source ${CmdletName}
							#$OldParentDisplayName is not needed because we are replacing it
							[Array]$AppKeyArr = Get-ChildItem -Path $Key	#Get all ArpKeyNames. Note: we do this twice. once for 64bit, another for 32bit ARP entries
							Write-Log "Looking in [$Key]," -Source ${CmdletName}
							Write-Log "for ARP entries with ParentKeyName = [$OldParentKeyName]" -Source ${CmdletName}
							ForEach ($AppKey in $AppKeyArr) {
								If ( $($AppKey.GetValue('ParentKeyName')) -eq $OldParentKeyName) {
									write-log "Found old parent [$OldParentKeyName] for [$($AppKey.GetValue('DisplayName'))] "  -Source ${CmdletName} -Severity 5
									Write-Log "[$($AppKey.PSChildName)] setting ParentKeyName to [$ParentKeyName]"  -Source ${CmdletName}
									Set-RegistryKey -Key $AppKey -Name ParentKeyName -Value $ParentKeyName -Type String
									Write-Log "[$($AppKey.PSChildName)] setting ParentDisplayName to [$ParentDisplayName]" -Source ${CmdletName}
									Set-RegistryKey -Key $AppKey -Name ParentDisplayName -Value $ParentDisplayName -Type String
									[Bool]$FoundArpKey = $true
								}
							}
							write-log "Replacing is done" -Source ${CmdletName} -Severity 5
						} Else {
							# check if UninstallString registry value exists. Do not make "ChildOfParent" if none exist as it causes problems (Matt found this issue)
							If (Test-RegistryValue -Key "$Key\$ArpKeyName" -Value UninstallString) {
								Write-log "Adding Parent values in [$ArpKeyName]..." -Source ${CmdletName}
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'ParentKeyName' -Value $ParentKeyName -ContinueOnError $false
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'ParentDisplayName' -Value $ParentDisplayName -ContinueOnError $false
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'SystemComponent' -Value 0 -Type DWord -ContinueOnError $true
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'WindowsInstaller' -Value 0 -Type DWord -ContinueOnError $true
							} ElseIf ($IgnoreMissingUninstallString) {
								Write-Log "[UninstallString] value in [$ArpKeyName] does not exist but -IgnoreMissingUninstallString is set to TRUE"
								Write-log "Adding Parent values in [$ArpKeyName]..." -Source ${CmdletName}
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'ParentKeyName' -Value $ParentKeyName -ContinueOnError $false
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'ParentDisplayName' -Value $ParentDisplayName -ContinueOnError $false
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'SystemComponent' -Value 0 -Type DWord -ContinueOnError $true
								Set-RegistryKey -Key "$Key\$ArpKeyName" -Name 'WindowsInstaller' -Value 0 -Type DWord -ContinueOnError $true
							} Else {
								Write-Log "[UninstallString] value  in [$ArpKeyName] does not exist. `r`nNot creating ChildOfParent ARP entries to avoid known issues" -Source ${CmdletName} -Severity 2
							}

							[Bool]$FoundArpKey = $true
						}
					} 
				}
				If ( -not $FoundArpKey) {
					If ($Remove) {
						[String]$Private:Message = "[$Key\$ArpKeyName] not found. `r`nNothing to remove"
						Write-log -Message $Private:Message -Severity 2 -Source ${CmdletName}
					} ElseIf ($OldParentKeyName) {
						[String]$Private:Message = "No ParentKeyName set to [$OldParentKeyName] were found. `r`nNothing to Replace/rename"
						Write-log -Message $Private:Message -Severity 2 -Source ${CmdletName}
					} Else {
						[String]$Private:Message = "[$Key\$ArpKeyName] not found. `r`nNo Children will be set for the Custom ARP entry. If this is desired, do not call [${CmdletName}] function for this package."
						Write-log -Message $Private:Message -Severity 3 -Source ${CmdletName}
						If (-not $ContinueOnError) {
							throw $Private:Message #this way the packager will definitely see the error before package is released.
						}
					}
				}
		} Catch {
			[string]$ErrorMessage = "$($_.Exception.Message) $($_.ScriptStackTrace) $($_.Exception.InnerException)"
			Write-Log $ErrorMessage -Severity 3 -Source ${CmdletName}
			Start-Sleep -Seconds 1
		}
	}
    End {
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
    }
}
#endregion Function Set-ARPChildOfParent


#region Function Set-LocalDiskUninstall
Function Set-LocalDiskUninstall {
<#
.SYNOPSIS
	Creates a local copy of installation files to permit LOCAL uninstallation from a custom ARP entry
.DESCRIPTION
	Creates a local copy of installation files to permit LOCAL uninstallation from a custom ARP entry
	Copies files from $scriptDirectory (the folder where Deploy-Application.ps1 is) and \SupportFiles\ folder to this local folder
	Creates the $configLocalUninstallCache\$installName folder if not exist
	NOTE: ARP = Add/Remove Programs = Programs and Features = Apps & Features
.PARAMETER PkgName
	Destination folder name where the installation files will be copied to.	Defaults to $installName
.PARAMETER Remove
	Removes this folder (for uninstallation)
.PARAMETER ContinueOnError
	Continue if unable to set or remove values or keys. Default is: $true
.PARAMETER CopyFilesFolder
	Use to copy the \Files\ folder too. Default is: $false
	Normally we don't need this folder and we can save lots of drive space
.EXAMPLE
	Set-LocalDiskUninstall
.EXAMPLE
	Set-LocalDiskUninstall -CopyFilesFolder $true
	Copy the \Files\ folder to $configLocalUninstallCache\$installName too.
	WARNING: You might be copying many GB worth locally just for uninstallation.
.EXAMPLE	
	Set-LocalDiskUninstall -PkgName $installName -Remove
.NOTES
	Author: Denis St-Pierre (Ottawa, Canada)
	-tested on Windows 10 and Windows 7
	$installName = BaseName of Deploy-Application_v1r1.ps1 file
	\Files\ is not copied by default otherwise there is no difference between \Files\ and \SupportFiles\
	The distinction is needed to save space on the local C: drive
	Only CMD and PS1 files in $scriptDirectory are copied. Place all files needed for uninstall in \SupportFiles\
#>
	[CmdletBinding()]
    Param (
        [Parameter(Mandatory=$false,HelpMessage="Name Of Package e.g. 'PkgName_v1r1'")]
        [ValidateNotNullorEmpty()]
		[Alias('FolderName')]
        [string]$PkgName = $installName,
		[Parameter(Mandatory=$false,HelpMessage="Removes the Local Uninstall folder")]
        [ValidateNotNullorEmpty()]
		[Switch]$Remove = $false,
		[Parameter(Mandatory=$false)]
		[ValidateNotNullorEmpty()]
		[boolean]$ContinueOnError = $true,
		[Parameter(Mandatory=$false,HelpMessage='Copy the \Files\ folder too.')]
		[ValidateNotNullorEmpty()]
		[boolean]$CopyFilesFolder = $false
    )
	Begin {
        ## Get the name of this function and write header
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
    }	
	Process {
		Try {

				[string]$PKGUNINSTALLDIR = "$configLocalUninstallCache\$PkgName"
				If ($Remove) {
					Write-Log "Removing Local Disk Uninstall Cache [$PKGUNINSTALLDIR]..." -Source ${CmdletName}
					Remove-Folder -Path $PKGUNINSTALLDIR -ContinueOnError $ContinueOnError
					Return #IOW Exit this function
				}
				
				If ( $PKGUNINSTALLDIR -eq $scriptDirectory ) {
					Write-Log "Running from Local Disk Uninstall Cache, not touching local cache."  -Source ${CmdletName}
				} else {
					Write-Log "Creating Local Disk Uninstall Cache folder [$PKGUNINSTALLDIR]."  -Source ${CmdletName}
					If ( Test-Path -Path "$PKGUNINSTALLDIR" ) {
						Write-Log "Local Disk Uninstall Cache directory [$PkgName] already exists."  -Source ${CmdletName}
						Write-Log "Overwriting..."  -Source ${CmdletName}
					}
					Write-Log "Copying files to [$PKGUNINSTALLDIR]..."  -Source ${CmdletName}
					New-Folder -Path $PKGUNINSTALLDIR  -ContinueOnError $ContinueOnError
					Copy-File -Path "$scriptDirectory\*.cmd" -Destination "$PKGUNINSTALLDIR" -ContinueOnError $ContinueOnError
					Copy-File -Path "$scriptDirectory\*.ps1" -Destination "$PKGUNINSTALLDIR" -ContinueOnError $ContinueOnError
					#NOTE: \Files\ is not copied otherwise there is no difference between \Files\ and \SupportFiles\ folders
					If ( Test-Path -Path "$scriptDirectory\SupportFiles") {
						Copy-File -Path "$scriptDirectory\SupportFiles" -Destination "$PKGUNINSTALLDIR" -Recurse  -ContinueOnError $ContinueOnError
					}
					Copy-File -Path "$scriptDirectory\AppDeployToolkit" -Destination "$PKGUNINSTALLDIR" -Recurse  -ContinueOnError $ContinueOnError
					If ( ( Test-Path -Path "$scriptDirectory\Files") -and $copyFilesFolder ) {
						Copy-File -Path "$scriptDirectory\Files" -Destination "$PKGUNINSTALLDIR" -Recurse  -ContinueOnError $ContinueOnError
					}
				}

		} Catch {
			[string]$ErrorMessage = "$($_.Exception.Message) $($_.ScriptStackTrace) $($_.Exception.InnerException)"
			Write-Log $ErrorMessage -Severity 3 -Source ${CmdletName}
		}
	}
    End {
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
    }	
}
#endregion Function Set-LocalDiskUninstall

Here are 2 other functions that might also be useful:


#region Function Set-ARP_SystemComponent
Function Set-ARP_SystemComponent {
<#
.SYNOPSIS
	Hides an ARP entry in the registry
.DESCRIPTION
	Hides an ARP entry bases on the name of a registry Key in the registry
	Auto-detects if key exists in 32bit or 64bit Registry ARP
	Not meant for MSIs. (Use SYSTEMCOMPONENT=1 MSI property instead)
	NOTE: ARP = Add/Remove Programs = Programs and Features = Apps & Features
.PARAMETER ArpKeyName
	Name of Registry key (*NOT* full registry path) that we want to hide or unhide
.PARAMETER Action
	set to 'UnHide' to unhide ARP entry. Omit to hide ARP entry
.PARAMETER ContinueOnError
	Continue if unable to set or remove values or keys. Default is: $false.
.EXAMPLE
	ARP_SYSTEMCOMPONENT_HideUnHide "Notepad++" Hide
.EXAMPLE
	ARP_SYSTEMCOMPONENT_HideUnHide "Notepad++" UnHide
.EXAMPLE
	ARP_SYSTEMCOMPONENT_HideUnHide "Notepad++" 			(Hide by  default)
.NOTES
	Author: Denis St-Pierre (Ottawa, Canada)
	-Tested on Windows 10 and Windows 7
#>
	[CmdletBinding()]
	Param (
	    [Parameter(Mandatory=$true,HelpMessage="Name of Registry key (Not full path)")]
		[string]$ArpKeyName,
		[Parameter(Mandatory=$false)]
		[ValidateNotNullorEmpty()]
		[ValidateSet("Hide","UnHide")]
		[string]$Action = "Hide"
	)
	Begin {
        ## Get the name of this function and write header
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
    }
	
	Process {
		Try {
			[string]$HKLMUninstallKey64 = "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$ArpKeyName"
			[string]$HKLMUninstallKey32 = "HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$ArpKeyName"

			#Where is the ARP key? 32 or 64 bit?
			If (Test-Path $HKLMUninstallKey32) {
				If ($Action -ceq "Hide") {
					Write-Log " Hiding ARP entry [$ArpKeyName] Exists as a 32bit ARP entry" -Source ${CmdletName}
					Set-RegistryKey -Key $HKLMUninstallKey32 -Name "SystemComponent" -Value 1 -Type DWORD
				} else {
					Write-Log " Unhiding ARP entry by deleting SystemComponent value [32bit]" -Source ${CmdletName}
					Remove-RegistryKey -Key $HKLMUninstallKey32 -Name "SystemComponent"
				}
			} else {
				Write-Log " [$ArpKeyName] is NOT a 32bit ARP entry" -Source ${CmdletName}
			}

			If (Test-Path $HKLMUninstallKey64) {
				If ($Action -ceq "Hide") {
					Write-Log " Hiding ARP entry [$ArpKeyName] Exists as a 64bit ARP entry" -Source ${CmdletName}
					Set-RegistryKey -Key $HKLMUninstallKey64 -Name "SystemComponent" -Value 1 -Type DWORD
				} else {
					Write-Log " Unhiding ARP entry by deleting SystemComponent value [64bit]" -Source ${CmdletName}
					Remove-RegistryKey -Key $HKLMUninstallKey64 -Name "SystemComponent"
				}
			} else {
				Write-Log " [$ArpKeyName] is not a 64bit ARP entry " -Source ${CmdletName}
			}
		} Catch {
			Write-Log -Message "Failed to edit ARP Entry. `r`n$(Resolve-Error)" -Severity 3 -Source ${CmdletName}
		}
	}
    End {
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
    }	
}
#endregion Function Set-ARP_SystemComponent


#region Function Set-ARPNoRemoveNoModifyNoRepair
Function Set-ARPNoRemoveNoModifyNoRepair {
<#
.SYNOPSIS
	Sets NoRemove/NoModify/NoRepair values for ARP entry
.DESCRIPTION
	Sets NoRemove/NoModify/NoRepair values for ARP entry based on the name of the Key in the registry
	Auto-detects if key exists in 32bit or 64bit Registry ARP
	Not meant for MSIs. (Use MSI properties instead)
	NOTE: ARP = Add/Remove Programs = Programs and Features = Apps & Features
.PARAMETER ArpKeyName
	Name of Registry key (*NOT* full registry path) that we want to affect (Defaults to $InstallName)
.PARAMETER Action
	set to ONE of the following: NoRemove NoModify NoRepair AllowRemove AllowModify AllowRepair
	CAVEAT: CaSe SenSiTive
.PARAMETER DeleteAction
	Controls HOW the value is removed
	set to ONE of the following: DeleteValue SetValueToZero 
.EXAMPLE
	Set-ARPNoRemoveNoModifyNoRepair -ArpKeyName "Notepad++" -Action NoRemove
	Hides [Remove] button in ARP by creating/setting the NoRemove value 1
.EXAMPLE
	Set-ARPNoRemoveNoModifyNoRepair -ArpKeyName "Notepad++" -Action AllowRemove 
	Deletes the NoRemove value to enable [Remove] button in ARP
.EXAMPLE
	Set-ARPNoRemoveNoModifyNoRepair -ArpKeyName "Notepad++" -Action AllowRemove -DeleteAction 'SetValueToZero' 
	Same as above but Sets the NoRemove value to '0' instead of deleting the NoRemove value
.EXAMPLE
	Set-ARPNoRemoveNoModifyNoRepair -Action NoRemove 	
	Hides [Remove] or [Uninstall] button in ARP for $InstallName
.EXAMPLE
	Set-ARPNoRemoveNoModifyNoRepair -ArpKeyName 'ProPlus2019Volume - en-us' -Action NoRemove
	Set-ARPNoRemoveNoModifyNoRepair -ArpKeyName 'ProPlus2019Volume - en-us' -Action AllowRemove
	Set-ARPNoRemoveNoModifyNoRepair -ArpKeyName 'ProPlus2019Volume - en-us' -Action AllowRemove -DeleteAction SetValueToZero
.NOTES
	Author: Denis St-Pierre (Ottawa, Canada)
	-Tested on Windows 10 and Windows 7
#>
	[CmdletBinding()]
	Param (
	    [Parameter(Mandatory=$true,HelpMessage="Name of Registry key (Not full path)")]
		[string]$ArpKeyName,
		[Parameter(Mandatory=$true,HelpMessage="Set to ONE of the following: NoRemove NoModify NoRepair AllowRemove AllowModify AllowRepair")]
		[ValidateNotNullorEmpty()]
		[ValidateSet('NoRemove','NoModify','NoRepair','AllowRemove','AllowModify','AllowRepair',IgnoreCase = $false)]
		[string]$Action = "",
		[Parameter(Mandatory=$false,HelpMessage="Set to ONE of the following: DeleteValue SetValueToZero. DeleteValue is the default" )]
		[ValidateNotNullorEmpty()]
		[ValidateSet('DeleteValue','SetValueToZero')]
		[string]$DeleteAction = "DeleteValue"		
	)
	Begin {
        ## Get the name of this function and write header
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -CmdletBoundParameters $PSBoundParameters -Header
    }
	
	Process {
		Try {
			[string]$HKLMUninstallKey64 = "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$ArpKeyName"
			[string]$HKLMUninstallKey32 = "HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$ArpKeyName"

			#Where is the ARP key? 32 or 64 bit?
			If (Test-Path -LiteralPath $HKLMUninstallKey32) {
				If ($Action -like "No*") {
					Write-Log " Setting [$Action] for ARP entry [$ArpKeyName] Exists as a 32bit ARP entry" -Source ${CmdletName}
					Set-RegistryKey -Key $HKLMUninstallKey32 -Name $Action -Value 1 -Type DWORD
				} ElseIf ($Action -like "Allow*") {
					[String]$ActualValueName=$Action.replace("Allow","No")
					If ($DeleteAction -eq 'DeleteValue'){
						Write-Log " Deleting [$ActualValueName] for ARP entry [$ArpKeyName] Exists as a 32bit ARP entry" -Source ${CmdletName}
						Remove-RegistryKey -Key $HKLMUninstallKey32 -Name $ActualValueName
					} ElseIf ($DeleteAction -eq 'SetValueToZero'){
						Write-Log " Setting Value [$ActualValueName] to 0 for ARP entry [$ArpKeyName] Exists as a 32bit ARP entry" -Source ${CmdletName}
						Set-RegistryKey -Key $HKLMUninstallKey32 -Name $ActualValueName -Value 0 -Type DWORD
					} Else {
					Write-Log "`$DeleteAction = [$DeleteAction] is not handled by this function" -Source ${CmdletName} -Severity 2
					}
				} Else {
					Write-Log "`$Action = [$Action] is not handled by this function" -Source ${CmdletName} -Severity 2
				}
			} Else {
				Write-Log " [$ArpKeyName] is NOT a 32bit ARP entry" -Source ${CmdletName}
			}

			If (Test-Path -LiteralPath $HKLMUninstallKey64) {
				If ($Action -like "No*") {
					Write-Log " Setting [$Action] for ARP entry [$ArpKeyName] Exists as a 64bit ARP entry" -Source ${CmdletName}
					Set-RegistryKey -Key $HKLMUninstallKey64 -Name $Action -Value 1 -Type DWORD
				} ElseIf ($Action -like "Allow*") {
					[String]$ActualValueName=$Action.replace("Allow","No")
					If ($DeleteAction -eq 'DeleteValue'){
						Write-Log " Deleting [$ActualValueName] for ARP entry [$ArpKeyName] Exists as a 64bit ARP entry" -Source ${CmdletName}
						Remove-RegistryKey -Key $HKLMUninstallKey64 -Name $ActualValueName
					} ElseIf ($DeleteAction -eq 'SetValueToZero'){
						Write-Log " Setting Value [$ActualValueName] to 0 for ARP entry [$ArpKeyName] Exists as a 64bit ARP entry" -Source ${CmdletName}
						Set-RegistryKey -Key $HKLMUninstallKey64 -Name $ActualValueName -Value 0 -Type DWORD
					} Else {
						Write-Log "`$DeleteAction = [$DeleteAction] is not handled by this function" -Source ${CmdletName} -Severity 2
					}
				} Else {
					Write-Log " [$Action] is not handled by this function" -Source ${CmdletName} -Severity 2
				}
			} Else {
				Write-Log " [$ArpKeyName] is NOT a 64bit ARP entry " -Source ${CmdletName}
			}
		} Catch {
			Write-Log -Message "Failed to edit ARP Entry. `r`n$(Resolve-Error)" -Severity 3 -Source ${CmdletName}
		}
	}
    End {
        Write-FunctionHeaderOrFooter -CmdletName ${CmdletName} -Footer
    }	
}
#endregion Function Set-ARPNoRemoveNoModifyNoRepair

Hi I am getting the following errors when using the ARP functions. It seems like the function doesn’t like an entry that is already installed.

[Installation] :: Failed to create Custom ARP enter[Deploy-Application]. 
Error Record:
-------------

Message        : Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is 
                 not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBE13B]
InnerException : System.Management.Automation.WildcardPatternException: The specified wildcard character pattern is 
                 not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBE13B]
                    at System.Management.Automation.WildcardPatternParser.AppendBracketExpression(String 
                 brackedExpressionContents, String bracketExpressionOperators, String pattern)
                    at System.Management.Automation.WildcardPatternParser.Parse(WildcardPattern pattern, 
                 WildcardPatternParser parser)
                    at 
                 System.Management.Automation.WildcardPatternMatcher.MyWildcardPatternParser.Parse(WildcardPattern 
                 pattern, CharacterNormalizer characterNormalizer)
                    at System.Management.Automation.WildcardPatternMatcher..ctor(WildcardPattern wildcardPattern)
                    at System.Management.Automation.WildcardPattern.Init()
                    at System.Management.Automation.WildcardPattern.IsMatch(String input)
                    at System.Management.Automation.LocationGlobber.IsChildNameAMatch(PSObject childObject, 
                 WildcardPattern stringMatcher, Collection`1 includeMatcher, Collection`1 excludeMatcher, String& 
                 childName)
                    at System.Management.Automation.LocationGlobber.GenerateNewPathsWithGlobLeaf(List`1 currentDirs, 
                 String leafElement, Boolean isLastLeaf, ContainerCmdletProvider provider, CmdletProviderContext 
                 context)
                    at System.Management.Automation.LocationGlobber.ExpandGlobPath(String path, Boolean 
                 allowNonexistingPaths, ContainerCmdletProvider provider, CmdletProviderContext context)
                    at System.Management.Automation.LocationGlobber.ResolveProviderPathFromProviderPath(String 
                 providerPath, String providerId, Boolean allowNonexistingPaths, CmdletProviderContext context, 
                 CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.ResolvePSPathFromProviderPath(String path, 
                 CmdletProviderContext context, Boolean allowNonexistingPaths, Boolean isProviderDirectPath, Boolean 
                 isProviderQualifiedPath, CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.GetGlobbedMonadPathsFromMonadPath(String path, 
                 Boolean allowNonexistingPaths, CmdletProviderContext context, CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.GetGlobbedProviderPathsFromMonadPath(String path, 
                 Boolean allowNonexistingPaths, CmdletProviderContext context, ProviderInfo& provider, CmdletProvider& 
                 providerInstance)
                    at System.Management.Automation.SessionStateInternal.GetPropertyDynamicParameters(String path, 
                 Collection`1 providerSpecificPickList, CmdletProviderContext context)
                    at Microsoft.PowerShell.Commands.GetItemPropertyCommand.GetDynamicParameters(CmdletProviderContext 
                 context)
                    at Microsoft.PowerShell.Commands.CoreCommandBase.GetDynamicParameters()
                    at System.Management.Automation.CmdletParameterBinderController.HandleCommandLineDynamicParameters(
                 ParameterBindingException& outgoingBindingException)

FullyQualifiedErrorId : GetDynamicParametersException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
ScriptStackTrace      : at <ScriptBlock>, C:\Restore\Toast\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 247
                        at Set-CustomARP<Process>, C:\Restore\Toast\AppDeployToolkit\AppDeployToolkitExtensions.ps1: 
                        line 247
                        at <ScriptBlock>, C:\Restore\Toast\Deploy-Application.ps1: line 244
                        at <ScriptBlock>, <No file>: line 1
                        at <ScriptBlock>, <No file>: line 1

PositionMessage : At C:\Restore\Toast\AppDeployToolkit\AppDeployToolkitExtensions.ps1:247 char:83
                  + ... rpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}
                  +                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



Error Inner Exception(s):
-------------------------

Message        : The specified wildcard character pattern is not valid: BeyondTrust Remote Support Jump Client 
                 [support.co.maui.hi.us-62FBE13B]
InnerException : 
[Installation] :: Failed to create Custom ARP enter[Deploy-Application]: Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBE13B] at Set-CustomARP<Process>, C:\Restore\Toast\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 311
at <ScriptBlock>, C:\Restore\Toast\Deploy-Application.ps1: line 244
at <ScriptBlock>, <No file>: line 1
at <ScriptBlock>, <No file>: line 1 

Since there is no code posted, I’m assuming you did something like:

Set-ARPChildOfParent -ArpKeyName 'BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBE13B]'

That’s a weird Registry key name for an ARP entry.
Is it at least in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall ?

PS: I posted this when I had time on my hands. I don’t have so much now but I’ll do my best.

Here is the weird part. I am not installing BeyondTrust. I am attempting to create an installation of loose files not an .msi or .exe. I will try to post some code later today.

Here are the main sections of my Deploy-Application.ps1

Variables:

## Variables: Install Titles (Only set here to override defaults set by the toolkit)
	
	[string]$installTitle = 'CoM Toast installer'	#used by Custom ARP, PSADT GUI prompts, PSADT balloonTip/toasts
	[string]$installName		= ($MyInvocation.MyCommand.Name).Replace('.ps1','')  # e.g. 'used by Custom ARP, localDiskUninstall, etc.
	[string]$AppMSIName			= '' # actual name of the MSI in the \Files folder
    [string]$AppMSICode			= '' # MSI Product Code of MSI above (used for removal)    [String]$installName = ''
    

Body:

##*===============================================
        ##* PRE-INSTALLATION
        ##*===============================================
        [String]$installPhase = 'Pre-Installation'

        ## Show Welcome Message, close Internet Explorer if required, allow up to 3 deferrals, verify there is enough disk space to complete the install, and persist the prompt
        #Show-InstallationWelcome -CloseApps 'iexplore' -AllowDefer -DeferTimes 3 -CheckDiskSpace -PersistPrompt

        ## Show Progress Message (with the default message)
        #Show-InstallationProgress

        ## <Perform Pre-Installation tasks here>


        ##*===============================================
        ##* INSTALLATION
        ##*===============================================
        [String]$installPhase = 'Installation'
        Set-LocalDiskUninstall	#Needed for Custom ARP entry to work

        ## Handle Zero-Config MSI Installations
        If ($useDefaultMsi) {
            [Hashtable]$ExecuteDefaultMSISplat = @{ Action = 'Install'; Path = $defaultMsiFile }; If ($defaultMstFile) {
                $ExecuteDefaultMSISplat.Add('Transform', $defaultMstFile)
            }
            Execute-MSI @ExecuteDefaultMSISplat; If ($defaultMspFiles) {
                $defaultMspFiles | ForEach-Object { Execute-MSI -Action 'Patch' -Path $_ }
            }
        }
        
        Copy-File -Path "$dirFiles\WindowsToast" -Destination "$envProgramData\WindowsToast" -Recurse
        ## <Perform Installation tasks here>
        #** Enable the code below to add install to Add Remove Programs.
        #Execute-MSI -Action Install -Path $AppMSIName -ContinueOnError $False -LogName "${AppMSIName}_MSI"
		#Set-ARPChildOfParent -ArpKeyName $AppMSICode #Hide ARP key created by MSI

        ##*===============================================
        ##* POST-INSTALLATION
        ##*===============================================
        [String]$installPhase = 'Post-Installation'

        ## <Perform Post-Installation tasks here>

        #This should be placed near the **end** of the INSTALLATION section
		#Set-CustomARP uses \SupportFiles\$InstallName.ico if possible, otherwise uses PSADT icon (AppDeployToolkitLogo.ico) for Custom ARP entry
		Set-CustomARP

        ## Display a message at the end of the install

My payload copies fine. I think that the Set-CustomARP function is parsing the Regsitry Uninstall locations and finding something it finds frustrating. Here are the current errors.

[Post-Installation] :: Failed to create Custom ARP enter[Deploy-Application]. 
Error Record:
-------------

Message        : Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is 
                 not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBDFD7]
InnerException : System.Management.Automation.WildcardPatternException: The specified wildcard character pattern is 
                 not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBDFD7]
                    at System.Management.Automation.WildcardPatternParser.AppendBracketExpression(String 
                 brackedExpressionContents, String bracketExpressionOperators, String pattern)
                    at System.Management.Automation.WildcardPatternParser.Parse(WildcardPattern pattern, 
                 WildcardPatternParser parser)
                    at 
                 System.Management.Automation.WildcardPatternMatcher.MyWildcardPatternParser.Parse(WildcardPattern 
                 pattern, CharacterNormalizer characterNormalizer)
                    at System.Management.Automation.WildcardPatternMatcher..ctor(WildcardPattern wildcardPattern)
                    at System.Management.Automation.WildcardPattern.Init()
                    at System.Management.Automation.WildcardPattern.IsMatch(String input)
                    at System.Management.Automation.LocationGlobber.IsChildNameAMatch(PSObject childObject, 
                 WildcardPattern stringMatcher, Collection`1 includeMatcher, Collection`1 excludeMatcher, String& 
                 childName)
                    at System.Management.Automation.LocationGlobber.GenerateNewPathsWithGlobLeaf(List`1 currentDirs, 
                 String leafElement, Boolean isLastLeaf, ContainerCmdletProvider provider, CmdletProviderContext 
                 context)
                    at System.Management.Automation.LocationGlobber.ExpandGlobPath(String path, Boolean 
                 allowNonexistingPaths, ContainerCmdletProvider provider, CmdletProviderContext context)
                    at System.Management.Automation.LocationGlobber.ResolveProviderPathFromProviderPath(String 
                 providerPath, String providerId, Boolean allowNonexistingPaths, CmdletProviderContext context, 
                 CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.ResolvePSPathFromProviderPath(String path, 
                 CmdletProviderContext context, Boolean allowNonexistingPaths, Boolean isProviderDirectPath, Boolean 
                 isProviderQualifiedPath, CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.GetGlobbedMonadPathsFromMonadPath(String path, 
                 Boolean allowNonexistingPaths, CmdletProviderContext context, CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.GetGlobbedProviderPathsFromMonadPath(String path, 
                 Boolean allowNonexistingPaths, CmdletProviderContext context, ProviderInfo& provider, CmdletProvider& 
                 providerInstance)
                    at System.Management.Automation.SessionStateInternal.GetPropertyDynamicParameters(String path, 
                 Collection`1 providerSpecificPickList, CmdletProviderContext context)
                    at Microsoft.PowerShell.Commands.GetItemPropertyCommand.GetDynamicParameters(CmdletProviderContext 
                 context)
                    at Microsoft.PowerShell.Commands.CoreCommandBase.GetDynamicParameters()
                    at System.Management.Automation.CmdletParameterBinderController.HandleCommandLineDynamicParameters(
                 ParameterBindingException& outgoingBindingException)

FullyQualifiedErrorId : GetDynamicParametersException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
ScriptStackTrace      : at <ScriptBlock>, 
                        C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 247
                        at Set-CustomARP<Process>, 
                        C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 247
                        at <ScriptBlock>, C:\Restore\ToastInstallerPackage\Deploy-Application.ps1: line 228
                        at <ScriptBlock>, <No file>: line 1
                        at <ScriptBlock>, <No file>: line 1

PositionMessage : At C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1:247 char:83
                  + ... rpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}
                  +                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



Error Inner Exception(s):
-------------------------

Message        : The specified wildcard character pattern is not valid: BeyondTrust Remote Support Jump Client 
                 [support.co.maui.hi.us-62FBDFD7]
InnerException : 
[Post-Installation] :: Failed to create Custom ARP enter[Deploy-Application]: Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBDFD7] at Set-CustomARP<Process>, C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 311
at <ScriptBlock>, C:\Restore\ToastInstallerPackage\Deploy-Application.ps1: line 228
at <ScriptBlock>, <No file>: line 1
at <ScriptBlock>, <No file>: line 1 

On my test machine, I uninstalled the “BeyondTrust Remote Support Jump Client” software restarted the computer and then tried my PSADT ARP deployment and the files copied and the ARP entry was created with no errors. The payload was also uninstalled from ARP control panel successfully.

It seems to be this section of code in the AppDeployTookitExtensions.ps1

It isn’t able to parse the key name which looks like this.


When i manually edit it to remove the brackets and the content inside to look like this.
reg3
The entry gets parsed by the code successfully and the ARP entry is created as expected.

I am not sure what to change to allow this section of code to parse the uninstall reg key with brackets and the content inside it from the pic above.

1 Like

I’m not use either but I’m impressed you resolved your issue.
For a while there I thought my code stopped working in some situations.

Unfortunately you might be correct. I am not sure this is really resolved. I have that bomgar agent on every desktop/laptop endpoint in our organization. I won’t be able to change the way that registry key is named.

I was wondering if you knew which section of code in your script would need to be changed to allow line 247 to parse the key properly. I think these errors point to a function or cmdlet that is using a regular expression to parse it… maybe?

Message        : Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is 
                 not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBDFD7]
InnerException : System.Management.Automation.WildcardPatternException: The specified wildcard character pattern is 
                 not valid: BeyondTrust Remote Support Jump Client [support.co.maui.hi.us-62FBDFD7]
                    at System.Management.Automation.WildcardPatternParser.AppendBracketExpression(String 
                 brackedExpressionContents, String bracketExpressionOperators, String pattern)
                    at System.Management.Automation.WildcardPatternParser.Parse(WildcardPattern pattern, 
                 WildcardPatternParser parser)
                    at 
                 System.Management.Automation.WildcardPatternMatcher.MyWildcardPatternParser.Parse(WildcardPattern 
                 pattern, CharacterNormalizer characterNormalizer)
                    at System.Management.Automation.WildcardPatternMatcher..ctor(WildcardPattern wildcardPattern)
                    at System.Management.Automation.WildcardPattern.Init()
                    at System.Management.Automation.WildcardPattern.IsMatch(String input)
                    at System.Management.Automation.LocationGlobber.IsChildNameAMatch(PSObject childObject, 
                 WildcardPattern stringMatcher, Collection`1 includeMatcher, Collection`1 excludeMatcher, String& 
                 childName)
                    at System.Management.Automation.LocationGlobber.GenerateNewPathsWithGlobLeaf(List`1 currentDirs, 
                 String leafElement, Boolean isLastLeaf, ContainerCmdletProvider provider, CmdletProviderContext 
                 context)
                    at System.Management.Automation.LocationGlobber.ExpandGlobPath(String path, Boolean 
                 allowNonexistingPaths, ContainerCmdletProvider provider, CmdletProviderContext context)
                    at System.Management.Automation.LocationGlobber.ResolveProviderPathFromProviderPath(String 
                 providerPath, String providerId, Boolean allowNonexistingPaths, CmdletProviderContext context, 
                 CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.ResolvePSPathFromProviderPath(String path, 
                 CmdletProviderContext context, Boolean allowNonexistingPaths, Boolean isProviderDirectPath, Boolean 
                 isProviderQualifiedPath, CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.GetGlobbedMonadPathsFromMonadPath(String path, 
                 Boolean allowNonexistingPaths, CmdletProviderContext context, CmdletProvider& providerInstance)
                    at System.Management.Automation.LocationGlobber.GetGlobbedProviderPathsFromMonadPath(String path, 
                 Boolean allowNonexistingPaths, CmdletProviderContext context, ProviderInfo& provider, CmdletProvider& 
                 providerInstance)
                    at System.Management.Automation.SessionStateInternal.GetPropertyDynamicParameters(String path, 
                 Collection`1 providerSpecificPickList, CmdletProviderContext context)
                    at Microsoft.PowerShell.Commands.GetItemPropertyCommand.GetDynamicParameters(CmdletProviderContext 
                 context)
                    at Microsoft.PowerShell.Commands.CoreCommandBase.GetDynamicParameters()
                    at System.Management.Automation.CmdletParameterBinderController.HandleCommandLineDynamicParameters(
                 ParameterBindingException& outgoingBindingException)

FullyQualifiedErrorId : GetDynamicParametersException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
ScriptStackTrace      : at <ScriptBlock>, 
                        C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 247
                        at Set-CustomARP<Process>, 
                        C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1: line 247
                        at <ScriptBlock>, C:\Restore\ToastInstallerPackage\Deploy-Application.ps1: line 228
                        at <ScriptBlock>, <No file>: line 1
                        at <ScriptBlock>, <No file>: line 1

PositionMessage : At C:\Restore\ToastInstallerPackage\AppDeployToolkit\AppDeployToolkitExtensions.ps1:247 char:83
                  + ... rpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}

While I have learned PowerShell out of necessity I haven’t taken any formal classes, so complex code like string parsing and arrays tend to overwhelm me at the moment. But I continue to learn :grinning::

I really like your plugin it works well when that bomgar agent isn’t installed. I have a few projects I want to use it in but because of the bomgar agent I might not be able to. Because those projects involve those same endpoints.

Any help would be greatly appreciated.

Here’s the chunk that is giving you grief:

[System.Array]$AllArpKeys = Get-ChildItem -Path $HKLMUninstallKey64bit -ErrorAction Stop
ForEach ($ArpKey in $AllArpKeys) {
	[PSObject]$Values = $ArpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}	#Your Line 247
	If ( $Values.ParentKeyName -eq $ArpKeyName) {
		[String]$BitNess = '64Bit'
		Write-Log "Found 64bit Child [$($Values.DisplayName)]	Keyname:[$($ArpKey.PSChildName)]" -Source ${CmdletName}
	}
}

The ForEach ($ArpKey in $AllArpKeys) loops through each 64bit ARP key.

Line 247 is just extracting the Key name in $ArpKey and putting it in $Values:
[PSObject]$Values = $ArpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop} #Your Line 247

Which key it’s processing at the time it blows up, we don’t know b/c we don’t log that info.

Please note this was written 10+ years ago and I’ve learned a lot since.
So try this simplified version of line 247:

[System.Array]$AllArpKeys = Get-ChildItem -Path $HKLMUninstallKey64bit -ErrorAction Stop
ForEach ($ArpKey in $AllArpKeys) {
	#[PSObject]$Values = $ArpKey | Foreach-Object {Get-ItemProperty $_.PsPath -ErrorAction Stop}	#Your Line 247
	#If ( $Values.ParentKeyName -eq $ArpKeyName) {
	If ( $($ArpKey.PSChildName) -eq $ArpKeyName) {
		[String]$BitNess = '64Bit'
		#Write-Log "Found 64bit Child [$($Values.DisplayName)]	Keyname:[$($ArpKey.PSChildName)]" -Source ${CmdletName}
		Write-Log "Found 64bit Child [$($ArpKey.GetValue('DisplayName')]	Keyname:[$($ArpKey.PSChildName)]" -Source ${CmdletName}
	}
}

This MIGHT work. If not, add the following line before the If and then we will know what it’s dying on:
Write-log "Comparing [$($ArpKey.PSChildName)].." -Source ${CmdletName}

There is a typo in the Write-Log function it is missing a ‘)’ It should be:

Write-Log "Found 64bit Child [$($ArpKey.GetValue('DisplayName'))]	Keyname:[$($ArpKey.PSChildName)]" -Source ${CmdletName}

After correcting the above typo. My initial testing, with your change, is working as expected.

A decade of use is pretty good congrats!

Thanks again for this extension and your help.

1 Like