Get-VMDKFileNamefromVMX

Working on some minor details to retrieve loads of data (we will see where this stuff ends-up..)

Here comes a minor Powershell function to retrieve all VMDK filenames from a VMware VMX-file. Output is the VMX-filename (so you know where it came from), the VMDK-files and the location of the VMX-file (so you know where to look for it).

Sample usage:

Get-VMDKFileNamefromVMX -VMX C:\VMs\VM1\vm1.vmx,c:\VMS\VM2\vm2.vmx

A sample output object;
output

function Get-VMDKFileNamefromVMX {
 <#
 .SYNOPSIS
 Parses a VMX-file for all VMDK-filenames
 .DESCRIPTION
 Outputs an object with all VMDK-filenames
 .EXAMPLE
 Get-VMDKFileNamefromVMX -VMX C:\VMs\VM1\vm1.vmx,c:\VMS\VM2\vm2.vmx
 #>
 [CmdletBinding()]
 param(
 [Parameter(Mandatory=$False, ValueFromPipeline=$true,
 HelpMessage="Location of VMX-File")]
 [alias("CFile")]
 [string[]]$VMX
 )
 Begin
 {
 $vmx = $vmx.split(",")
 write-verbose "------------------------"
 write-verbose "Start of Get-VMDKFileNamefromVMX"
 Write-Verbose "VMX-files: $($vmx.count)"
 }
 Process
 { 

foreach ($file in $vmx)
 {
 write-verbose "Search for VMDK in $($file)"
 try
 {
 $vmdkfiles = Select-String -Path $($file) -Pattern vmdk
 }
 catch
 {
 write-error "Failed to retrieve $($file)"
 }
 write-verbose "Parsing results for VMDK"
 write-verbose "Found $($vmdkfiles.count) matches of VMDK"
 foreach ($vmdk in $vmdkfiles)
 {

write-verbose "Found: $($vmdk.line)"
 $vmdkfilename = ($vmdk.line).split("=")[1]
 $vmdkfilename = $vmdkfilename.Replace("`"","")
 $vmdkfilename = $vmdkfilename.trim()
 $object = New-Object –TypeName PSObject
 $object | Add-Member –MemberType NoteProperty –Name VMX –Value $($file)
 $object | Add-Member –MemberType NoteProperty –Name VMDK –Value $($vmdkfilename)
 $object | Add-Member –MemberType NoteProperty –Name Location –Value $(Split-Path $file)
 $object
 }
 } 

 }
 End
 {
 write-verbose "End of Get-VMDKFileNamefromVMX"
 write-verbose "------------------------"
 }
 } 

Test-Latency

A need arose to determine the latency to a few different nodes and act on that matter. Someone on the internet had almost already written all the Powershell code I wanted. However the code was primarily focused on outputting the results in a CSV-file and not actually using the result in the best aus online casino code afterwards.

Therefore I have re-written this function to output an object instead.

 

#####################################
## Based on Ping-Latency
## Rewritten by Nicke Källén
## nicke dot kallen at applepie dot se
## Original header:
## http://kunaludapi.blogspot.com
## Version: 1
## Tested this script on
##  1) Powershell v3
##  2) Windows 7
##
#####################################
function Test-Latency {
 &lt;#
 .SYNOPSIS
 Uses Test-Connection and determines latency to a host
 .DESCRIPTION
 Outputs each node with Hostname, IP-Address, Latency (ms) and Date
 .EXAMPLE
 Test-Latency -ComputerNames 192.168.0.1,google.com

 #&gt;
 [CmdletBinding()]
 param(
 [Parameter(Mandatory=$False, ValueFromPipeline=$true,
 HelpMessage="Hostnames or IP-Address seperated by commas")]
 [alias("Computer")]
 [string[]]$ComputerNames = $env:COMPUTERNAME
 )
 Begin {}
 Process
 { 

 $ComputerNames = $ComputerNames.split(",")
 foreach ($Computer in $ComputerNames)
 {
 $Response = Test-Connection -ComputerName $computer -Count 1 -ErrorAction SilentlyContinue
 if ($Response -eq $null)
 {
 $object = New-Object –TypeName PSObject
 $object | Add-Member –MemberType NoteProperty –Name Hostname –Value $Computer
 $object | Add-Member –MemberType NoteProperty –Name IPaddress –Value "Unreachable"
 $object | Add-Member –MemberType NoteProperty –Name Latency –Value "No response"
 $object | Add-Member –MemberType NoteProperty –Name Date –Value $(Get-Date)
 $object
 }
 else
 {
 $object = New-Object –TypeName PSObject
 $object | Add-Member –MemberType NoteProperty –Name Hostname –Value $($Computer)
 $object | Add-Member –MemberType NoteProperty –Name IPAddress –Value $($Response.IPV4Address)
 $object | Add-Member –MemberType NoteProperty –Name Latency –Value $($Response.ResponseTime)
 $object | Add-Member –MemberType NoteProperty –Name Date –Value $(Get-Date)
 $object
 }
 } 

 }
 End {}
 }

Uninstall Software

Based on the previous post handling the removal of the Ask software (the beloved add-on that everyone joyfully installs along with Java) a more developed script took form to handle any type of software.

Its based on the following borrowed pieces of code,

Get-LHSInstInstalledApp has been extended to also output the installationdate. Apart from that everything is as is from the original function

Convert-DateString has been used to convert the InstallationDate string to a date that can be used for calculations

ExitWithCode is a function that is simply used to end the script with an accumulated Exit Code from all uninstallations.

The script will accept best canada online casinos the following parameters;

ApplicationName – a wild card search for the applications we want to remove.

PublisherName – we can validate that the right publisher have installed the application

InstallDateOlder – amount of days since the application was installed for us to remove it. Standard is 30

IgnoreInstallDate – True / False – we can choose to completely ignore when the application was installed

If the application is something other than an MSI – it will just report that a productcode is missing and not attempt the installation.

A log-file will be created in %WINDIR%\TEMP\APP_(yourappname)_Removal.LOG

Each uninstall will have a log-file written in %WINDIR%\TEMP with AP_UNINSTALL as prefix.

 

 

Running the script requires admin permissions

 

#========================================================================
# Created with: PowerShell ISE
# Created on: 2015-02-21 23:32
# Created by: Nicke Källén
# Organization: Applepie.se
# Filename: SCCM_Uninstall_Unused_Application
# Comment: Uninstalls an application (msi support only) based
# on Display Name in ARP, Publisher and how long ago
# it was installed
# Convert-DateString function
# http://www.powershellmagazine.com/2013/07/08/pstip-
# converting-a-string-to-a-system-datetime-object/
# Get-LHSInstalledApp - appended InstallDate to output
# https://gallery.technet.microsoft.com/scriptcenter/
# Get-Installed-Application-615fa73a
# Exit function
# http://weblogs.asp.net/soever/returning-an-exit-
# code-from-a-powershell-script
#========================================================================
param (
 [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$True)]
 [string]$ApplicationName = "",
 [Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$false)]
 [string]$PublisherName = "",
 [int]$InstallDateOlder = "30",
 [Parameter(mandatory=$false)]
 [bool]$IgnoreInstallDate=$false
 )

 function Convert-DateString ([String]$Date, [String[]]$Format)
{
 $result = New-Object DateTime

 $convertible = [DateTime]::TryParseExact(
 $Date,
 $Format,
 [System.Globalization.CultureInfo]::InvariantCulture,
 [System.Globalization.DateTimeStyles]::None,
 [ref]$result)

 if ($convertible) { $result }
}

Function Get-LHSInstalledApp {
&lt;#
.SYNOPSIS
 List installed applications for local or remote computers.

.DESCRIPTION
 List installed applications for local or remote computers.

 List both 32-bit and 64-bit applications. Note that
 dotNet 4.0 Support for Powershell 2.0 needed.

 Output looks like this:
 -------------------------
 ComputerName : N104100
 AppID : {90120000-001A-0407-0000-0000000FF1CE}
 AppName : Microsoft Office Outlook MUI (German) 2007
 Publisher : Microsoft Corporation
 Version : 12.0.6612.1000
 Architecture : 32bit
 UninstallString : MsiExec.exe /X{90120000-001A-0407-0000-0000000FF1CE} 

.PARAMETER ComputerName
 Outputs applications for the named computer(s).
 If you omit this parameter, the local computer is assumed.

.PARAMETER AppID
 Outputs applications with the specified application ID.
 An application's appID is equivalent to its subkey name underneath the Uninstall registry key.
 For Windows Installer-based applications, this is the application's product code GUID
 (e.g. {3248F0A8-6813-11D6-A77B-00B0D0160060}). Wildcards are permitted.

.PARAMETER AppName
 Outputs applications with the specified application name.
 The AppName is the application's name as it appears in the
 Add/Remove Programs list. Wildcards are permitted.

.PARAMETER Publisher
 Outputs applications with the specified publisher name.
 Wildcards are permitted

.PARAMETER Version
 Outputs applications with the specified version.
 Wildcards are permitted.

.EXAMPLE
 PS C:\&gt; Get-LHSInstalledApp

 This command outputs installed applications on the current computer.

.EXAMPLE
 PS C:\&gt; Get-LHSInstalledApp | Select-Object AppName,Version | Sort-Object AppName

 This command outputs a sorted list of applications on the current computer.

.EXAMPLE
 PS C:\&gt; Get-LHSInstalledApp wks1,wks2 -Publisher "*microsoft*"

 This command outputs all installed Microsoft applications on the named computers.
 * regular expression to match any characters.

.EXAMPLE
 PS C:\&gt; Get-LHSInstalledApp wks1,wks2 -AppName "*Office 97*" 

 This command outputs any Application Name that match "Office 97" on the named computers.
 * regular expression to match any characters.

.EXAMPLE
 PS C:\&gt; Get-Content ComputerList.txt | Get-LHSInstalledApp -AppID "{1A97CF67-FEBB-436E-BD64-431FFEF72EB8}" | Select-Object ComputerName

 This command outputs the computer names named in ComputerList.txt that have the specified application installed.

.EXAMPLE
 Get-LHSInstalledApp | Where-Object {-not ( $_.AppID -like "KB*") } |
 ConvertTo-CSV -Delimiter ';' -NoTypeInformation | Out-File -FilePath C:\temp\AppsInfo.csv
 Invoke-Item C:\temp\AppsInfo.csv

 Outputs all installed application except KB fixes to an CSV file and opens in Excel

.INPUTS
 System.String, you can pipe ComputerNames to this Function

.OUTPUTS
 PSObjects containing the following properties:

 ComputerName - computer where the application is installed
 AppID - the application's AppID
 AppName - the application's name
 Publisher - the application's publisher
 Version - the application's version
 Architecture - the application's architecture (32-bit or 64-bit)
 UninstallString - the application uninstall String

.NOTES
 More Info:
 ==========
 Why not using Get-WmiObject
 ---------------------------
 * Win32_Product
 At first glance, Win32_Product would appear to be one of those best solutions.
 The Win32_product class is not query optimized.
 Queries such as “select * from Win32_Product where (name like 'Sniffer%')”
 require WMI to use the MSI provider to enumerate all of the installed
 products and then parse the full list sequentially to handle the “where” clause:,

 * This process initiates a consistency check of packages installed,
 and then verifying and repairing the installations.
 * If you have an application that makes use of the Win32_Product class,
 you should contact the vendor to get an updated version that does not use this class.

 On Windows Server 2003, Windows Vista, and newer operating systems, querying Win32_Product
 will trigger Windows Installer to perform a consistency check to verify the health of the
 application. This consistency check could cause a repair installation to occur. You can
 confirm this by checking the Windows Application Event log. You will see the following
 events each time the class is queried and for each product installed:

 Event ID: 1035
 Description: Windows Installer reconfigured the product. Product Name: &lt;ProductName&gt;.
 Product Version: &lt;VersionNumber&gt;. Product Language: &lt;languageID&gt;.
 Reconfiguration success or error status: 0.

 Event ID: 7035/7036
 Description: The Windows Installer service entered the running state.

 I would not recommend querying Win32_Product in your production environment unless you are in a maintenance window.

 * Win32Reg_AddRemovePrograms
 Win32Reg_AddRemovePrograms is not a standard Windows class.
 This WMI class is only loaded during the installation of an SMS/SCCM client.

 What is great about Win32Reg_AddRemovePrograms is that it contains similar properties and
 returns results noticeably quicker than Win32_Product.

 Using Registry:
 ----------------
 By default, if your process is running as a 32 bit process you will end up accessing the 32 bit "reflection" of
 the remote system. Therefore, registry keys like HKLM\Software will actually be mapped to HKLM\Software\Wow6432Node
 which gets very frustrating! You can access the 64 bit "reflection" via WMI, but personally I find that quite painful.

 Fortunately, in .NET 4, the registry class had some extra features added to it which allowed for a new
 overload "RegistryView". Therefore, you can now specify exactly which "reflection" of the registry
 you want to access and manipulate! No more headaches!

 In order to use this function, the Powershell instance must support .Net 4.0 or greater, which is fairly straightforward if you follow these instructions.
 1. Open notepad and copy the below text exactly as shown into the document.

&lt;?xml version="1.0"?&gt;
&lt;configuration&gt; &lt;startup useLegacyV2RuntimeActivationPolicy="true"&gt; &lt;supportedRuntime version="v4.0.30319"/&gt; &lt;supportedRuntime version="v2.0.50727"/&gt; &lt;/startup&gt;
&lt;/configuration&gt;

 2. Save this document as c:\windows\System32\WindowsPowerhsell\v1.0\Powershell.exe.config
 (and/or c:\windows\System32\WindowsPowerhsell\v1.0\Powershell_ise.exe.config)
 (in addition for the 32bit Powershell on a 64bit Windows C:\Windows\SysWOW64\WindowsPowerShell\v1.0\*.config)
 3. Reload powershell and type the following command: $PsVersionTable.clrVersion (It should show Major version 4 if .Net 4 is supported.)

 NAME: Get-LHSInatalledApp.ps1
 AUTHOR: u104018
 LASTEDIT: 02/06/2012 16:01:40
 KEYWORDS: Registry Redirection, Installed software, Registry64, WOW6432Node,Accessing Remote x64 Registry From an x86/x32 OS Computer

.LINK
 http://poshcode.org/3186
 http://blogs.technet.com/b/heyscriptingguy/archive/2011/11/13/use-powershell-to-quickly-find-installed-software.aspx
 http://msdn.microsoft.com/en-us/library/aa393067%28VS.85%29.aspx

#Requires -Version 2.0
#&gt;

[cmdletbinding(DefaultParameterSetName = 'Default', ConfirmImpact = 'low')] 

Param(

 [Parameter(ParameterSetName='AppID', Position=0,Mandatory=$False,ValueFromPipeline=$True)]
 [Parameter(ParameterSetName='Default', Position=0,Mandatory=$False,ValueFromPipeline=$True)]
 [string[]] $ComputerName=$ENV:COMPUTERNAME,

 [Parameter(ParameterSetName='AppID', Position=1)]
 [String] $AppID = "*",

 [Parameter(ParameterSetName='Default', Position=1)]
 [String] $AppName = "*",

 [Parameter(ParameterSetName='Default', Position=2)]
 [String] $Publisher = "*",

 [Parameter(ParameterSetName='Default', Position=3)]
 [String] $Version = "*"

 )

BEGIN {
 ${CmdletName} = $Pscmdlet.MyInvocation.MyCommand.Name

 If (!($PsVersionTable.clrVersion.Major -ge 4)) {Write-Error "Requires .Net 4.0 support for Powershell 2.0"; Return} 

} # end BEGIN

PROCESS {
 #Write-Verbose -Message "${CmdletName}: Starting Process Block"
 ForEach ($Computer in $ComputerName) {
 Write-Verbose "`$Computer contains $Computer"
 IF (Test-Connection -ComputerName $Computer -Count 2 -Quiet) {
 try { 

 Write-Verbose "Get Architechture Type of the system"
 $OSArch = (Get-WMIObject -ComputerName $Computer win32_operatingSystem -ErrorAction Stop).OSArchitecture
 if ($OSArch -like "*64*") {$Architectures = @("32bit","64bit")}
 else {$Architectures = @("32bit")}
 #Create an array to capture program objects.
 $arApplications = @()
 foreach ($Architecture in $Architectures){
 #We have a 64bit machine, get the 32 bit software.
 if ($Architecture -like "*64*"){
 #Define the entry point to the registry.
 $strSubKey = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
 $SoftArchitecture = "32bit"
 $RegViewEnum = [Microsoft.Win32.RegistryView]::Registry64
 }
 #We have a 32bit machine, use the 32bit registry provider.
 elseif ($Architectures -notcontains "64bit"){
 #Define the entry point to the registry.
 $strSubKey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
 $SoftArchitecture = "32bit"
 $RegViewEnum = [Microsoft.Win32.RegistryView]::Registry32
 }
 #We have "64bit" in our array, capture the 64bit software.
 else{
 #Define the entry point to the registry.
 $strSubKey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
 $SoftArchitecture = "64bit"
 $RegViewEnum = [Microsoft.Win32.RegistryView]::Registry64
 }

 Write-Verbose "Create a remote registry connection to the Computer."
 $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computer, $RegViewEnum)
 $RegKey = $Reg.OpenSubKey($strSubKey)

 Write-Verbose "Get all subkeys that exist in the entry point."
 $RegSubKeys = $RegKey.GetSubKeyNames() 

 Write-Debug "Architecture : $Architecture"
 Write-Debug "SoftArchitecture : $SoftArchitecture"
 Write-Verbose "Enumerate the subkeys."
 foreach ($SubKey in $RegSubKeys)
 {
 Write-Debug "`$SubKey : $SubKey"
 $Program = $Reg.OpenSubKey("$strSubKey\\$SubKey")
 $strDisplayName = $Program.GetValue("DisplayName")
 if ($strDisplayName -eq $NULL) { continue } # skip entry if empty display name

 switch ($PsCmdlet.ParameterSetName)
 { 

 "AppID" { if ((split-path $SubKey -leaf) -like $AppID)
 {
 $RegKey = ("HKLM\$strSubKey\$SubKey").replace("\\","\")

 $output = new-object PSObject
 $output | add-member NoteProperty "ComputerName" -value $computer
 $output | add-member NoteProperty "RegKey" -value ($RegKey) # useful when debugging
 $output | add-member NoteProperty "AppID" -value (split-path $SubKey -leaf)
 $output | add-member NoteProperty "AppName" -value $strDisplayName
 $output | add-member NoteProperty "Publisher" -value $Program.GetValue("Publisher")
 $output | add-member NoteProperty "Version" -value $Program.GetValue("DisplayVersion")
 $output | add-member NoteProperty "Architecture" -value $SoftArchitecture
 $output | add-member NoteProperty "UninstallString" -value $Program.GetValue("UninstallString")
 $output | add-member NoteProperty "InstallDate" -value $Program.GetValue("InstallDate")

 $output
 } #end if
 } #end "AppID"

 "Default" { If (( $strDisplayName -like $AppName ) -and (
 $Program.GetValue("Publisher") -like $Publisher ) -and (
 $Program.GetValue("DisplayVersion") -like $Version ))
 {
 $RegKey = ("HKLM\$strSubKey\$SubKey").replace("\\","\")

 $output = new-object PSObject
 $output | add-member NoteProperty "ComputerName" -value $computer
 $output | add-member NoteProperty "RegKey" -value ($RegKey) # useful when debugging
 $output | add-member NoteProperty "AppID" -value (split-path $SubKey -leaf)
 $output | add-member NoteProperty "AppName" -value $strDisplayName
 $output | add-member NoteProperty "Publisher" -value $Program.GetValue("Publisher")
 $output | add-member NoteProperty "Version" -value $Program.GetValue("DisplayVersion")
 $output | add-member NoteProperty "Architecture" -value $SoftArchitecture
 $output | add-member NoteProperty "UninstallString" -value $Program.GetValue("UninstallString")
 $output | add-member NoteProperty "InstallDate" -value $Program.GetValue("InstallDate")

 $output
 } #end if
 } #end "Default"
 } #end switch

 } # end foreach ($SubKey in $RegSubKeys)
 } # end foreach ($Architecture in $Architectures)
 } Catch {
 write-error $_
 }
 } Else {
 Write-Warning "\\$Computer DO NOT reply to ping"
 } # end IF (Test-Connection -ComputerName $Computer -count 2 -quiet)
 } # end ForEach ($Computer in $computerName)

} # end PROCESS

END { Write-Verbose "Function ${CmdletName} finished." }

} # end Function Get-LHSInatalledApp

function Log
{
 [cmdletbinding()]
 param (
 [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
 [string]$text,
 [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$False)]
 [string]$filename
 )
 begin
 {
 Write-Debug 'Logging starting'
 Write-Debug "Filename: $($filename)"
 }
 process
 {
 foreach ($txt in $text)
 {
 Out-File $filename -append -noclobber -inputobject $txt -encoding ASCII
 Write-Verbose $txt
 }

 }

 end
 {
 Write-Debug 'Logging ending'
 }

}

function ExitWithCode
{
 param
 ($exitcode)

 Write-Verbose "Ending with $($ExitCode)"
 $host.SetShouldExit($exitcode)
 exit
}

function Remove-InstalledMSI
{
 [cmdletbinding()]
 param (
 [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$True,ValueFromPipelineByPropertyName)]
 [ValidatePattern('\{.+\}')]
 [Alias('AppID')]
 [string]$ProductCode = $null,
 [Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$False)]
 [string]$LogFilePath,
 [Parameter(Position=1,Mandatory=$false,ValueFromPipeline=$False)]
 [string]$Property
 )

 begin
 {
 Write-Verbose 'Start of Remove-InstallMSI'
 $exitcode = $null
 } 

 process
 {

 foreach ($pc in $ProductCode)
 {
 Write-Verbose "Uninstall ProductCode: $($pc)"
 $AppName = $((Get-LHSInstalledApp -AppID $pc).AppName)
 Write-Verbose "AppName: $($AppName)"
 if ($LogFilePath -ne $null)
 {
 $LogFilePath = "c:\windows\TEMP\AP_UNINSTALL_$($AppName).log"
 }
 else
 {
 Write-verbose "LogFilePath: $LogFilePath"
 }
 $argumentlist = "/x $pc /qn REBOOT=ReallySuppress /lv `"$($LogFilePath)`" "
 $argumentlist += $Property
 Write-Verbose "Argument List: $($argumentlist)" 

 $exitcode = (Start-Process -filepath "msiexec.exe" -ArgumentList $argumentlist -Wait -PassThru).ExitCode
 Write-Verbose "Exit Code: $($exitcode)"

 $output = new-object PSObject
 $output | add-member NoteProperty "ProductCode" -value $pc
 $output | add-member NoteProperty "AppName" -value $AppName
 $output | add-member NoteProperty "ExitCode" -value $($ExitCode)
 $output | add-member NoteProperty "LogFilePath" -value $LogFilePath

 $output
 }

 }

 end
 {
 Write-Verbose 'End of Remove-InstallMSI'
 }

}

$logfile = "$env:windir\temp\AP_$($ApplicationName)_Removal.log"
Write-Verbose "Logfile: $($logfile)"
try { Remove-Item $logfile -force -ErrorAction SilentlyContinue }
catch { Write-Warning $_ }

log $logfile '--------------------------------------------'
log $logfile "$(get-date) - $($ApplicationName) - Removal Started"
log $logfile "$(get-date) - $($ApplicationName) - Searching for $($PublisherName) $($ApplicationName)"
 if ($IgnoreInstallDate -eq $true)
 {
 Write-Verbose "IgnoreInstallDate set $($IgnoreInstallDate)"
 log $logfile "$(get-date) - $($ApplicationName) - Ignoring installation date"
 }
 else
 {
 Write-Verbose "IgnoreInstallDate set $($IgnoreInstallDate)"
 log $logfile "$(get-date) - $($ApplicationName) - Removal only if install date is more than $($InstallDateOlder) days ago"
 }

if ($PublisherName)
{

 $applist = Get-LHSInstalledApp -AppName "*$($ApplicationName)*" -Publisher "*$($PublisherName)*"
}
else
{
 $applist = Get-LHSInstalledApp -AppName "*$($ApplicationName)*"
}

log $logfile "$(get-date) - $($ApplicationName) - Found $($applist.Count) $($ApplicationName) installations"
log $logfile '--------------------------------------------'
$applist | foreach { write-verbose "AppName: $($_.AppName)"}
$ReturnValue = 0
Write-Verbose "Current Exit Code: $($ReturnValue)"

$applist | where {$_.appid -notmatch ('\{.+\}') } | foreach { log $logfile "$(get-date) - $($_.AppName) has no productcode" }
$applist = $applist | where {$_.appid -match ('\{.+\}') } 

if ($IgnoreInstallDate -eq $true)
{
 $uninstall = $applist | Remove-InstalledMSI
}
else
{
 $uninstall = $applist | where-object { ($_.InstallDate -notin ($null,'')) -and`
 ( ((get-date) - (Convert-DateString -Date $($_.InstallDate) -Format 'yyyyMMdd')).days -gt $InstallDateOlder ) }`
 | Remove-InstalledMSI
}

$uninstall | foreach { log $logfile "$(get-date) - $($_.AppName) - Exit Code: $($_.ExitCode)" ; $returnvalue += $_.exitcode }

log $logfile '--------------------------------------------'
log $logfile "$(get-date) - $($ApplicationName) - Removal Finished"

ExitWithCode($ReturnValue)

 

 

 

Computer cleanup – Ask removal

Got a lot of computers with the Ask-suite of toolbars installed? When users are allowed administrative permissions on their end-points this is a likely scenario – here is a minor script bit that will cleanup the computer from the hassle.

First – retrieve the Get-LHSInstInstalledApp function from the Technet Gallery. Its a very well written function to retrieve information about both 32-bit and 64-bit applications installed within a Windows environment – great for retrieving information about the installed applications on a computer.

I am not certain if I wrote the Log function on my own, or if this is just something that I grabbed from a script someone previously wrote. If you did write it – just give a shout!

 

Running the script requires admin permissions

 

function Log
{
param([string]$filename,[string]$text)
Out-File $filename -append -noclobber -inputobject $text -encoding ASCII
}
$logfile = "$env:windir\temp\SMS_Ask_Removal.log"
Remove-Item $logfile -force
log $logfile "$(get-date) - Ask Removal Started"

$ask = Get-LHSInstalledApp -AppName *Ask* -Publisher "APN, LLC"

log $logfile "$(get-date) - Found $($ask.Count) Ask installations"

Foreach ($install in $ask)
{
log $logfile "--------------------------------------------"
log $logfile "$(get-date) - $($install.AppName) found"
try
{
log $logfile "$(get-date) - $($install.AppName) removal"
$exitcode = (Start-Process -filepath "msiexec.exe" -ArgumentList "/x $($install.appid) /qn REBOOT=ReallySuppress /lv `"c:\windows\TEMP\UNINSTALL_$($install.AppName).log`"" -Wait -PassThru).ExitCode
log $logfile "$(get-date) - $($install.AppName) - return code: $exitcode"
}
catch
{
log $logfile "$(get-date) - ERROR: $($install.AppName) removal failed"
}

}

log $logfile "--------------------------------------------"
log $logfile "$(get-date) - Ask Removal Finished" 

 

 

 

App-V 5, ConfigMgr compliance and fixes

App-V 5 has recently gotten hit by two odd behaviors relating to an update and .NET Framework 4.5.2.

KB2984972 was released as an update for RDC, and caused some havoc both for App-V 4 and App-V 5. The workaround is documented in the article and essentially allows anyone to remove the unfortunate end-user experience by adding some registry keys.

.NET Framework 4.5.2 was released and quite early on people started noticing that a freeze could be experienced when using certain App-V applications. The culprit seems to be the processes wisptis.exe, and the issue could temporarily be worked around by terminating the process.

App-V 5 got hit by two issues that both were resolved by adding registry keys under the registry key ObjExclusions. The Gladiator has written an article that details more about this registry key, the purpose of it and the effects of it. The article is focused on App-V 4, however the knowledge and concepts still apply to App-V 5.

Under (HKLM\Software\Microsoft\AppV\Subsystem\ObjExclusions) this registry key there are a lot of registry keys starting at 1 and going upwards. Each registry key contains a value (oh, really?) that is the name of an object that is not virtualized.  Anyone can append new values by using the next available number. Aaron Parker wrote a great article on howto leverage Group Policy to add the requested registry keys to resolve the issues for KB2984972.

Let’s detail the fun fact about this registry key;

There are in a default installation of App-V registry keys from 1-92. On any given default installation the next available number we can use is 93. We now have two issues and would therefore end up with two extra registry keys (93 and 94). My guess is that Microsoft might potentially include these two above recommended registry keys in a future installation of App-V when a new version comes out. Forcing these values to be added to a specific number in the series could potentially throw other valuable exclusions out the window…

Therefore I personally voted against Group Policy (Preferences) and decided to go the route of ConfigMgr Compliance Settings.

By creating a configuration item I can achieve the following;

Detect if the specific value is already in the list
Find the next available number to create a new registry key in
Append the value if it doesn’t already exist.

In the end, this is what I came up with;

appv_ci

Detect if the App-V client is installed;

appv_detection

Two checks for each specific registry key;

appv_check

Create a rule set that will allow for remediation;

appv_wsptis

Scripts part of the Configuration Item. This sample is from the fix for KB2984972.

Check:

$regKey = "HKLM:\SOFTWARE\Microsoft\AppV\Subsystem\ObjExclusions"
$p = Get-ItemProperty $regKey
$kb2984972 = $p.PSObject.Properties | where { $_.Name -match "[0-9]" -and $_.Value -eq "TermSrvReadyEvent" } | select-object -ExpandProperty Name -ErrorAction SilentlyContinue

if(($? -and ($kb2984972 -ne $null))) {
1
}
else {
-1
}

Remediation:

$regKey = "HKLM:\SOFTWARE\Microsoft\AppV\Subsystem\ObjExclusions"
$p = Get-ItemProperty $regKey
$topvalue = $p.PSObject.Properties | Where-Object { $_.Name -match "[0-9]" } | Sort-Object -Property Name -Descending | Select-Object -first 1 -ExpandProperty Name
$topvalue = 1 + $topvalue

Function New-RegistryKey([string]$key,[string]$Name,[string]$type,[string]$value)

{

#Split the registry path into its single keys and save

#them in an array, use \ as delimiter:

$subkeys = $key.split("\")

#Do this for all elements in the array:

foreach ($subkey in $subkeys)

{

#Extend $currentkey with the current element of

#the array:

$currentkey += ($subkey + '\')

#Check if $currentkey already exists in the registry

if (!(Test-Path $currentkey))

{

#If no, create it and send Powershell output

#to null (don't show it)

New-Item -Type String $currentkey | Out-Null

}

}

#Set (or change if alreday exists) the value for $currentkey

Set-ItemProperty $CurrentKey $Name -value $Value -type $type

}

New-RegistryKey $regkey $topvalue "String" "TermSrvReadyEvent"

As a final treat. Here is the Configuration Item – ready to be imported into ConfigMgr.

 

2014-12-07 – Sebastian Gern stated an additional registry key for WISPTIS. The Configuration Item is also updated with the new settings.

AppsNotify – Ping the Application Catalog

One downside with Configuration Manager 2012 compared to Configuration Manager 2007 is that applications deployed to users, only made available, will not give a notification on the endpoint. This caused some initial confusion that needed a blog-post to clarify the matter – as all admins were used to a single view on the client what could be installed.

Microsoft created the Application Catalog, and for newer devices (mobile and Windows 8+) they made a secondary interface called the Company Portal (read about deployment at Justin Chalfants blog). However, none of these give a notification to the end user if an application is only made available.

Therefore I created a script in PowerShell that can just do that – ping the Application Catalog – check if there are new apps, and if so notify the user.

How does it work?

A scheduled task is setup

image

It starts the application 15 minutes after logon (to avoid excessive workload..), and then runs every 15 minutes.

After that, the workflow is something like;

Connect to Application Catalog

  • If no previous check is completed, gather a list.No notification is given.
  • If a previous check is completed, compare it.
    • No difference? Do nothing
    • New applications available? Notify the user
      • Maintain a notification in the system tray (which directs the user to the Application Catalog if clicked)
    • Applications removed? Update the list, no notification to the end-user

 

For each check there is a log file within the users %TEMP% called appsnotify app.log. Sample output;

image

How does it look?

Like this;

appsnotify

 

How do I deploy it?

MSI-file to install it

To install it use the MSI file with the property APPCATALOG. Input should be;

APPCATALOG=http://localhost/CMApplicationCatalog

(no slash at the end)

The Application Catalog needs to be prepared for client interaction which is described by Microsoft in a blog-article. See the Getting Started section of Extending the Application Catalog in System Center 2012 Configuration Manager.

How do I make it my own?

In all Community spirit, here comes the code / files.

XML-file for creating the scheduled task – incase you want to build your own

&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-16&quot;?&gt;
&lt;Task version=&quot;1.2&quot; xmlns=&quot;http://schemas.microsoft.com/windows/2004/02/mit/task&quot;&gt;
  &lt;RegistrationInfo&gt;
    &lt;Author&gt;PRECISION\Nicke&lt;/Author&gt;
  &lt;/RegistrationInfo&gt;
  &lt;Triggers&gt;
    &lt;LogonTrigger&gt;
      &lt;Repetition&gt;
        &lt;Interval&gt;PT15M&lt;/Interval&gt;
        &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt;
      &lt;/Repetition&gt;
      &lt;StartBoundary&gt;1899-12-30T06:04:14&lt;/StartBoundary&gt;
      &lt;Enabled&gt;true&lt;/Enabled&gt;
      &lt;Delay&gt;PT15M&lt;/Delay&gt;
    &lt;/LogonTrigger&gt;
  &lt;/Triggers&gt;
  &lt;Principals&gt;
    &lt;Principal id=&quot;Author&quot;&gt;
      &lt;GroupId&gt;S-1-5-32-545&lt;/GroupId&gt;
      &lt;RunLevel&gt;LeastPrivilege&lt;/RunLevel&gt;
    &lt;/Principal&gt;
  &lt;/Principals&gt;
  &lt;Settings&gt;
    &lt;MultipleInstancesPolicy&gt;IgnoreNew&lt;/MultipleInstancesPolicy&gt;
    &lt;DisallowStartIfOnBatteries&gt;false&lt;/DisallowStartIfOnBatteries&gt;
    &lt;StopIfGoingOnBatteries&gt;false&lt;/StopIfGoingOnBatteries&gt;
    &lt;AllowHardTerminate&gt;false&lt;/AllowHardTerminate&gt;
    &lt;StartWhenAvailable&gt;true&lt;/StartWhenAvailable&gt;
    &lt;RunOnlyIfNetworkAvailable&gt;true&lt;/RunOnlyIfNetworkAvailable&gt;
    &lt;IdleSettings&gt;
      &lt;StopOnIdleEnd&gt;true&lt;/StopOnIdleEnd&gt;
      &lt;RestartOnIdle&gt;false&lt;/RestartOnIdle&gt;
    &lt;/IdleSettings&gt;
    &lt;AllowStartOnDemand&gt;true&lt;/AllowStartOnDemand&gt;
    &lt;Enabled&gt;true&lt;/Enabled&gt;
    &lt;Hidden&gt;true&lt;/Hidden&gt;
    &lt;RunOnlyIfIdle&gt;false&lt;/RunOnlyIfIdle&gt;
    &lt;WakeToRun&gt;false&lt;/WakeToRun&gt;
    &lt;ExecutionTimeLimit&gt;PT0S&lt;/ExecutionTimeLimit&gt;
    &lt;Priority&gt;7&lt;/Priority&gt;
  &lt;/Settings&gt;
  &lt;Actions Context=&quot;Author&quot;&gt;
    &lt;Exec&gt;
      &lt;Command&gt;C:\Program Files (x86)\Common Files\AppsNotify\AppsNotify 2.0.exe&lt;/Command&gt;
      &lt;Arguments&gt;-appcatalog http://www.sample.com&lt;/Arguments&gt;
    &lt;/Exec&gt;
  &lt;/Actions&gt;
&lt;/Task&gt;

Source code for the PowerShell script. Wrapped it into a .exe with PowerShell Studio 2012. I have only tested this on Windows 7 x64 with PowerShell 3.0 / 2.0. Log-functions are from 9to5it

#========================================================================
# Created on: 2014-06-10
# Created by: Nicke Källén
# Organization:
# Filename: AppsNotify 2.0.pff
#========================================================================

$AppNotify.FormBorderStyle = 'FixedToolWindow'
Function Log-Start{
 &lt;#
 .SYNOPSIS
 Creates log file

 .DESCRIPTION
 Creates log file with path and name that is passed. Checks if log file exists, and if it does deletes it and creates a new one.
 Once created, writes initial logging data

 .PARAMETER LogPath
 Mandatory. Path of where log is to be created. Example: C:\Windows\Temp

 .PARAMETER LogName
 Mandatory. Name of log file to be created. Example: Test_Script.log

 .PARAMETER ScriptVersion
 Mandatory. Version of the running script which will be written in the log. Example: 1.5

 .INPUTS
 Parameters above

 .OUTPUTS
 Log file created

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support

 .EXAMPLE
 Log-Start -LogPath &quot;C:\Windows\Temp&quot; -LogName &quot;Test_Script.log&quot; -ScriptVersion &quot;1.5&quot;
 #&gt;

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName, [Parameter(Mandatory=$true)][string]$ScriptVersion)

 Process{
 $sFullPath = $LogPath + &quot;\&quot; + $LogName

 #Check if file exists and delete if it does
 If((Test-Path -Path $sFullPath)){
 Remove-Item -Path $sFullPath -Force
 }

 #Create file and start logging
 New-Item -Path $LogPath -Name $LogName –ItemType File

 Add-Content -Path $sFullPath -Value &quot;***************************************************************************************************&quot;
 Add-Content -Path $sFullPath -Value &quot;Started processing at [$([DateTime]::Now)].&quot;
 Add-Content -Path $sFullPath -Value &quot;***************************************************************************************************&quot;
 Add-Content -Path $sFullPath -Value &quot;&quot;
 Add-Content -Path $sFullPath -Value &quot;Running script version [$ScriptVersion].&quot;
 Add-Content -Path $sFullPath -Value &quot;&quot;
 Add-Content -Path $sFullPath -Value &quot;***************************************************************************************************&quot;
 Add-Content -Path $sFullPath -Value &quot;&quot;

 #Write to screen for debug mode
 Write-Debug &quot;***************************************************************************************************&quot;
 Write-Debug &quot;Started processing at [$([DateTime]::Now)].&quot;
 Write-Debug &quot;***************************************************************************************************&quot;
 Write-Debug &quot;&quot;
 Write-Debug &quot;Running script version [$ScriptVersion].&quot;
 Write-Debug &quot;&quot;
 Write-Debug &quot;***************************************************************************************************&quot;
 Write-Debug &quot;&quot;

 }
}

Function Log-Write{
 &lt;#
 .SYNOPSIS
 Writes to a log file

 .DESCRIPTION
 Appends a new line to the end of the specified log file

 .PARAMETER LogPath
 Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

 .PARAMETER LineValue
 Mandatory. The string that you want to write to the log

 .INPUTS
 Parameters above

 .OUTPUTS
 None

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support

 .EXAMPLE
 Log-Write -LogPath &quot;C:\Windows\Temp\Test_Script.log&quot; -LineValue &quot;This is a new line which I am appending to the end of the log file.&quot;
 #&gt;

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LineValue)

 Process{
 Add-Content -Path $LogPath -Value $LineValue

 #Write to screen for debug mode
 Write-Debug $LineValue
 }
}

Function Log-Error{
 &lt;#
 .SYNOPSIS
 Writes an error to a log file

 .DESCRIPTION
 Writes the passed error to a new line at the end of the specified log file

 .PARAMETER LogPath
 Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

 .PARAMETER ErrorDesc
 Mandatory. The description of the error you want to pass (use $_.Exception)

 .PARAMETER ExitGracefully
 Mandatory. Boolean. If set to True, runs Log-Finish and then exits script

 .INPUTS
 Parameters above

 .OUTPUTS
 None

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support. Added -ExitGracefully parameter functionality

 .EXAMPLE
 Log-Error -LogPath &quot;C:\Windows\Temp\Test_Script.log&quot; -ErrorDesc $_.Exception -ExitGracefully $True
 #&gt;

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$ErrorDesc, [Parameter(Mandatory=$true)][boolean]$ExitGracefully)

 Process{
 Add-Content -Path $LogPath -Value &quot;Error: An error has occurred [$ErrorDesc].&quot;

 #Write to screen for debug mode
 Write-Debug &quot;Error: An error has occurred [$ErrorDesc].&quot;

 #If $ExitGracefully = True then run Log-Finish and exit script
 If ($ExitGracefully -eq $True){
 Log-Finish -LogPath $LogPath
 Break
 }
 }
}

Function Log-Finish{
 &lt;#
 .SYNOPSIS
 Write closing logging data &amp; exit

 .DESCRIPTION
 Writes finishing logging data to specified log and then exits the calling script

 .PARAMETER LogPath
 Mandatory. Full path of the log file you want to write finishing data to. Example: C:\Windows\Temp\Test_Script.log

 .PARAMETER NoExit
 Optional. If this is set to True, then the function will not exit the calling script, so that further execution can occur

 .INPUTS
 Parameters above

 .OUTPUTS
 None

 .NOTES
 Version: 1.0
 Author: Luca Sturlese
 Creation Date: 10/05/12
 Purpose/Change: Initial function development

 Version: 1.1
 Author: Luca Sturlese
 Creation Date: 19/05/12
 Purpose/Change: Added debug mode support

 Version: 1.2
 Author: Luca Sturlese
 Creation Date: 01/08/12
 Purpose/Change: Added option to not exit calling script if required (via optional parameter)

 .EXAMPLE
 Log-Finish -LogPath &quot;C:\Windows\Temp\Test_Script.log&quot;

 .EXAMPLE
 Log-Finish -LogPath &quot;C:\Windows\Temp\Test_Script.log&quot; -NoExit $True
 #&gt;

 [CmdletBinding()]

 Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$false)][string]$NoExit)

 Process{
 Add-Content -Path $LogPath -Value &quot;&quot;
 Add-Content -Path $LogPath -Value &quot;***************************************************************************************************&quot;
 Add-Content -Path $LogPath -Value &quot;Finished processing at [$([DateTime]::Now)].&quot;
 Add-Content -Path $LogPath -Value &quot;***************************************************************************************************&quot;

 #Write to screen for debug mode
 Write-Debug &quot;&quot;
 Write-Debug &quot;***************************************************************************************************&quot;
 Write-Debug &quot;Finished processing at [$([DateTime]::Now)].&quot;
 Write-Debug &quot;***************************************************************************************************&quot;

 #Exit calling script if NoExit has not been specified or is set to False
 If(!($NoExit) -or ($NoExit -eq $False)){
 Exit
 }

 }
}

function Get-ScriptDirectory
{
if($hostinvocation -ne $null)
{
Split-Path $hostinvocation.MyCommand.path
}
else
{
Split-Path $script:MyInvocation.MyCommand.Path
}
} 

function Parse-Commandline
{
&lt;#
 .SYNOPSIS
 Parses the Commandline of a package executable

 .DESCRIPTION
 Parses the Commandline of a package executable

 .PARAMETER Commandline
 The Commandline of the package executable

 .EXAMPLE
 $arguments = Parse-Commandline -Commandline $Commandline

 .INPUTS
 System.String

 .OUTPUTS
 System.Collections.Specialized.StringCollection
#&gt;

 [OutputType([System.Collections.Specialized.StringCollection])]
 Param([string]$CommandLine) 

 $Arguments = New-Object System.Collections.Specialized.StringCollection 

 if($CommandLine)
 {
 #Find First Quote
 $index = $CommandLine.IndexOf('&quot;') 

 while ( $index -ne -1)
 {#Continue as along as we find a quote
 #Find Closing Quote
 $closeIndex = $CommandLine.IndexOf('&quot;',$index + 1)
 if($closeIndex -eq -1)
 {
 break #Can’t find a match
 }
 $value = $CommandLine.Substring($index + 1,$closeIndex – ($index + 1))
 [void]$Arguments.Add($value)
 $index = $closeIndex 

 #Find First Quote
 $index = $CommandLine.IndexOf('&quot;',$index + 1)
 }
 }
 return $Arguments
}

function Convert-CommandLineToDictionary
{
 &lt;#
 .SYNOPSIS
 Parses and converts the commandline of a packaged executable into a Dictionary

 .DESCRIPTION
 Parses and converts the commandline of a packaged executable into a Dictionary

 .PARAMETER Dictionary
 The Dictionary to load the value pairs into.

 .PARAMETER CommandLine
 The commandline of the package executable

 .PARAMETER ParamIndicator
 The character used to indicate what is a parameter.

 .EXAMPLE
 $Dictionary = New-Object System.Collections.Specialized.StringDictionary
 Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline -ParamIndicator '-'
 #&gt;
 Param( [ValidateNotNull()]
 [System.Collections.Specialized.StringDictionary]$Dictionary,
 [string]$CommandLine,
 [char] $ParamIndicator) 

 $Params = Parse-Commandline $CommandLine

 for($index = 0; $index -lt $Params.Count; $index++)
 {
 [string]$param = $Params[$index]
 #Clear the values
 $key = &quot;&quot;
 $value = &quot;&quot; 

 if($param.StartsWith($ParamIndicator))
 {
 #Remove the indicator
 $key = $param.Remove(0,1)
 if($index + 1 -lt $Params.Count)
 {
 #Check if the next Argument is a parameter
 [string]$param = $Params[$index + 1]
 if($param.StartsWith($ParamIndicator) -ne $true )
 {
 #If it isn’t a parameter then set it as the value
 $value = $param
 $index++
 }
 }
 $Dictionary[$key] = $value
 }#else skip
 }
}

function Validate-IsURL
{
 &lt;#
 .SYNOPSIS
 Validates if input is an URL

 .DESCRIPTION
 Validates if input is an URL

 .PARAMETER Url
 A string containing an URL address

 .INPUTS
 System.String

 .OUTPUTS
 System.Boolean
 #&gt;
 [OutputType([Boolean])]
 param ([string]$Url)

 if($Url -eq $null)
 {
 return $false
 }

 return $Url -match &quot;^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;amp;%\$#_]*)?$&quot;
}

function Get-CMUserApps {
 [CmdletBinding()]
 param
 (
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='URL for Application Catalogue')]
 $url,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Path to logfile')]
 $logfile,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Temp-file')]
 $temp
 )
 Begin {
 log-write -LogPath $logfile -LineValue &quot;Create web service proxy&quot;
 $catalogurl = $url;
 Log-Write -LogPath $logfile -LineValue &quot;Connecting to $catalogurl&quot;
 try {
 $url = $catalogurl+&quot;/ApplicationViewService.asmx?WSDL&quot;;
 $service = New-WebServiceProxy $url -UseDefaultCredential;

 }
 catch {
 Log-Error -LogPath $logfile -ErrorDesc &quot;AppCatalog no response&quot; -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $true
 break
 }
 }
 Process {

 $total = 0;
 try {
 Log-Write -LogPath $logfile -LineValue &quot;Gathering applications&quot;
 $service.GetApplications(&quot;Name&quot;,$null,&quot;Name&quot;,&quot;&quot;,100,0,$true,&quot;PackageProgramName&quot;,$false,$null,[ref]$total) | select ApplicationId,Name | Export-Clixml $temp
 return $true
 }

 catch {
 Log-Error -LogPath $logfile -ErrorDesc $error[0] -ExitGraceFully $false
 return $false
 }
 Remove-Variable -Name url
 Remove-Variable -Name total
 $service.dispose()

 }

}

function Compare-CMUserApps {
 [CmdletBinding()]
 param
 (
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Permanent-file')]
 $file,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Temp-file')]
 $temp,
 [Parameter(Mandatory=$True,
 ValueFromPipelineByPropertyName=$True,
 HelpMessage='Path to logfile')]
 $logfile
 )
Process {
 Log-Write -LogPath $logfile -LineValue &quot;Comparing applications lists&quot;
 If (-Not (Test-Path $file)) {
 Log-Write -LogPath $logfile -LineValue &quot;No previous version of apps list&quot;
 try {
 Rename-Item $temp &quot;$prefix apps.xml&quot;
 }
 catch {
 Remove-Item $temp
 Log-Error -LogPath $logfile -ErrorDesc &quot;Unable to create initial list&quot; -ExitGracefully $false
 }

 }
 Else {

 Log-Write -LogPath $logfile -LineValue &quot;Starting check......&quot;
 # $diffs = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '&lt;='}
 # $diffsserver = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '=&gt;'}

 If ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) -eq $null) {
 Log-Write -LogPath $logfile -LineValue &quot;No new applications&quot;
 Log-Write -LogPath $logfile -LineValue &quot;Removing temporary file&quot;

 try {
 Remove-Item $temp
 }
 catch {

 Log-Error -LogPath $logfile -ErrorDesc &quot;Unable to remove temp list&quot; -ExitGracefully $false
 }

 }
 Elseif (((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '&lt;='}) -ne $null -and ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '=&gt;'}) -eq $null ) {
 Log-Write -LogPath $logfile -LineValue &quot;Less applications received&quot;
 try {
 Log-Write -LogPath $logfile -LineValue &quot;Remove permanent list&quot;
 Remove-Item $file
 }
 catch {
 Remove-Item $temp
 Log-Error -LogPath $logfile -ErrorDesc &quot;Unable to remove permanent list&quot; -ExitGracefully $false
 }

 try {
 Log-Write -LogPath $logfile -LineValue &quot;Rename temporary list&quot;
 Rename-Item $temp &quot;$prefix apps.xml&quot;
 }
 catch {
 Log-Error -LogPath $logfile -ErrorDesc &quot;Unable to switch temp-list to permanent&quot; -ExitGracefully $false
 }

 }

 Else {
 Log-Write -LogPath $logfile -LineValue &quot;New applications found&quot;

 $newapps = $true

 }

 }
 If ($newapps -eq $true) {
 return $True

 }

 }

}

function OnApplicationLoad {
 #Note: This function is not called in Projects
 #Note: This function runs before the form is created
 #Note: To get the script directory in the Packager use: Split-Path $hostinvocation.MyCommand.path
 #Note: To get the console output in the Packager (Windows Mode) use: $ConsoleOutput (Type: System.Collections.ArrayList)
 #Important: Form controls cannot be accessed in this function
 #TODO: Add snapins and custom code to validate the application load

 return $true #return true for success or false for failure
}

function OnApplicationExit {
 #Note: This function is not called in Projects
 #Note: This function runs after the form is closed
 #TODO: Add custom code to clean up and unload snapins when the application exits
 #Log-Finish -LogPath $logfilePath -NoExit $true
 $script:ExitCode = 0 #Set the exit code for the Packager
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

$AppNotify_Load={
 #TODO: Initialize Form Controls here
 $NotifyIcon.ShowBalloonTip(30000,&quot;New Application&quot;,&quot;You have new applications available&quot;, 'Info')
}

#region Control Helper Functions
function Show-NotifyIcon
{
&lt;#
 .SYNOPSIS
 Displays a NotifyIcon's balloon tip message in the taskbar's notification area.

 .DESCRIPTION
 Displays a NotifyIcon's a balloon tip message in the taskbar's notification area.

 .PARAMETER NotifyIcon
 The NotifyIcon control that will be displayed.

 .PARAMETER BalloonTipText
 Sets the text to display in the balloon tip.

 .PARAMETER BalloonTipTitle
 Sets the Title to display in the balloon tip.

 .PARAMETER BalloonTipIcon
 The icon to display in the ballon tip.

 .PARAMETER Timeout
 The time the ToolTip Balloon will remain visible in milliseconds.
 Default: 0 - Uses windows default.
#&gt;
 param(
 [Parameter(Mandatory = $true, Position = 0)]
 [ValidateNotNull()]
 [System.Windows.Forms.NotifyIcon]$NotifyIcon,
 [Parameter(Mandatory = $true, Position = 1)]
 [ValidateNotNullOrEmpty()]
 [String]$BalloonTipText,
 [Parameter(Position = 2)]
 [String]$BalloonTipTitle = '',
 [Parameter(Position = 3)]
 [System.Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'None',
 [Parameter(Position = 4)]
 [int]$Timeout = 0
 )

 if($NotifyIcon.Icon -eq $null)
 {
 #Set a Default Icon otherwise the balloon will not show
 $NotifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon([System.Windows.Forms.Application]::ExecutablePath)
 }

 $NotifyIcon.ShowBalloonTip($Timeout, $BalloonTipTitle, $BalloonTipText, $BalloonTipIcon)
}

#endregion

$NotifyIcon_MouseDoubleClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
 #TODO: Place custom script here
 Log-Write -LogPath $logfilePath -LineValue &quot;User clicked icon&quot;
 Log-Write -LogPath $logfilePath -LineValue &quot;Sending user to $appcatalog&quot;
 Start-Process $appcatalog
 $NotifyIcon.Visible = $false

 try {
 Log-Write -LogPath $logfilePath -LineValue &quot;Removing $filepath&quot;
 Remove-Item $filePath
 Log-Write -LogPath $logfilePath -LineValue &quot;Renaming $tempfilePath&quot;
 Rename-Item -Path &quot;$tempfilePath&quot; -NewName &quot;$prefix apps.xml&quot; -Force

 }
 catch {
 Remove-Item $tempfilePath
 Log-Error -LogPath $logfile -ErrorDesc &quot;Unable to remove permanent list&quot; -ExitGracefully $false
 }
 $AppNotify.Close()
 $NotifyIcon.Dispose()
 #Log-Finish -LogPath $logfilePath -NoExit $false
 #$AppNotify.Close()
 #$timer1.Start()

}

$NotifyIcon_MouseClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
 $NotifyIcon.Visible = $true
 $NotifyIcon.ShowBalloonTip(30000,&quot;New Application&quot;,&quot;You have new applications available&quot;, 'Info')
}

$NotifyIcon_BalloonTipClicked={
 Log-Write -LogPath $logfilePath -LineValue &quot;User clicked ballontip&quot;
 Log-Write -LogPath $logfilePath -LineValue &quot;Sending user to $appcatalog&quot;
 Start-Process $appcatalog
 $NotifyIcon.Visible = $false

 try {
 Log-Write -LogPath $logfilePath -LineValue &quot;Removing $filepath&quot;
 Remove-Item $filePath
 Log-Write -LogPath $logfilePath -LineValue &quot;Renaming $tempfilePath&quot;
 Rename-Item -Path &quot;$tempfilePath&quot; -NewName &quot;$prefix apps.xml&quot; -Force

 }
 catch {
 Remove-Item $tempfilePath
 Log-Error -LogPath $logfile -ErrorDesc &quot;Unable to remove permanent list&quot; -ExitGracefully $false
 }
 $AppNotify.Close()
 $NotifyIcon.Dispose()
 #Log-Finish -LogPath $logfilePath -NoExit $true
 #exit
 #$timer1.Start()
}

#Get path which scripts run from
$CurrentPath = Get-ScriptDirectory

#Prefix for all generated files in user's %TEMP%
$prefix = &quot;appsnotify&quot;

#Logfile
$logfilePath = $env:temp+&quot;\$prefix app.log&quot;

$check=Get-Process AppsNotify -ErrorAction SilentlyContinue | Measure-Object

if ($check.count -lt &quot;2&quot;) {

}
else {
 Log-Error -LogPath $logfilePath -ErrorDesc &quot;AppsNotify is already running. Terminating. &quot; -ExitGraceFully $false
 exit
}

#Temporary file to store applications
$tempfilePath = $env:temp+&quot;\$prefix app_temp.xml&quot;
#Permanent file to store applications
$filePath = $env:temp +&quot;\$prefix apps.xml&quot;
#Reset log-file for this session
Remove-Item $logfilePath

################################################################################################################
Log-Start -LogPath $env:temp -LogName &quot;$prefix app.log&quot; -ScriptVersion &quot;2.0&quot;

#Verify that the $CommandLine variable exists
if($CommandLine -ne $null -and $CommandLine -ne &quot;&quot;)
{
 #Log-Write -LogPath $logfilePath -LineValue &quot;There is a command-line&quot;
 Log-Write -LogPath $logfilePath -LineValue &quot;Command-line is:&quot;
 Log-Write -LogPath $logfilePath -LineValue &quot;$CommandLine&quot;
 #$Arguments = Parse-Commandline $CommandLine
 #Convert the Arguments. Use – as the Argument Indicator
 $Dictionary = New-Object System.Collections.Specialized.StringDictionary
 Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline -ParamIndicator '-'
}
else
{
 #Not running in a packager or no command line arguments passed
 Log-Error -LogPath $logfilePath -ErrorDesc &quot;No command-line argument. Use -appcatalog &lt;url&gt;&quot; -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

$appcatalog = $Dictionary[&quot;appcatalog&quot;]
if($appcatalog -ne $null -and $appcatalog -ne &quot;&quot;) {
 Log-Write -LogPath $logfilePath -LineValue &quot;Passed Application Catalog is $appcatalog&quot;
 if (Validate-IsURL -Url $appcatalog) {
 Log-Write -LogPath $logfilePath -LineValue &quot;Passed Application Catalog is a URL&quot;
 }
 Else {
 Log-Error -LogPath $logfilePath -ErrorDesc &quot;This is not a url&quot; -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
 }

}
else {
 #Address to Application Catalogue
 Log-Error -LogPath $logfilePath -ErrorDesc &quot;We need an Application Catalog&quot; -ExitGraceFully $false
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

if ((Get-CMUserApps -url $appcatalog -logfile $logfilePath -temp $tempfilePath) -eq $true) {

 if ((Compare-CMUserApps -file $filePath -temp $tempfilePath -logfile $logfilePath) -eq $true) {

 try {
 $NotifyIcon.Visible = $true
 }
 catch {
 Log-Write -LogPath $logfilePath -LineValue &quot;Exception&quot;
 }
 $NotifyIcon.ShowBalloonTip(30000,&quot;New Application&quot;,&quot;You have new applications available&quot;, 'Info')

 }
 Else {
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
 }

}
Else {
 Log-Finish -LogPath $logfilePath -NoExit $false
 break
}

$NotifyIcon_BalloonTipShown={
 #TODO: Place custom script here
 Log-Write -LogPath $logfilePath -LineValue &quot;Notifying user&quot;
}

} 

Redistribute Failed Packages in ConfigMgr

Since the topic of redistributing failed packages is quite often surfacing in larger environments and there are quite a few PowerShell scripts out there to achieve this.

David O´Brien has written a PowerShell script that redistributes all packages that has any state (but successfull) to all DPs. In a larger environment this would be very risky (consider the amount of bandwidth you could potentially consume).

David went about the task by looking up the current state of the SMS_PackageStatusDistPointsSummarizer which has 7 states of a package , and then looping through all packages for all DPs and initiate the operation RefreshNow for each package and DP.

Within SCCM 2012 R2 there seems to be 9 possible states of a package, where a state 7 and 8 seems to be undocumented. State 7 would indicate that the source-files were not reachable for the SCCM 2012 server, and State 8 would indicate that a package validation failed (for any reason).

Quite often the need is more targeted and in particular we are required to only verify a single package or distribution point. As we would go through the console to check the state of a package and look under Content Status to see – it would be easiest to simply trigger a redistribute action for all DPs that are reported as failed. Previously Greg Ramsey released the great tool to start the action Validate All DPs, which can be initiated from any package under Content Status. Great tool! Lets take that one step further and create two additional menus within Configuration Manager console!

Redistribute a package to all DPs where it failed under Content Status

image

image


Param(
[Parameter(Mandatory=$true)]
[String]$SiteSrv,
[Parameter(Mandatory=$false)]
[String]$SiteNamespace,
[Parameter(Mandatory=$True)]
[String]$PackageID
)

$SiteCode = $SiteNamespace.Substring(14)

Write-Host "Checking" $PackageID -ForegroundColor red
Write-host "Will check for Installed Failed status (3) and Validation Failed (8)"
Write-Host ""

$sOpt = New-CimSessionOption –Protocol DCOM
$SiteServerCIM = New-CimSession -ComputerName $SiteSrv -SessionOption $sOpt

$DPs =  Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_PackageStatusDistPointsSummarizer -Filter "PackageID = '$($PackageID)' AND (State = '3' OR State = '8')"
if ($DPs -ne $null)
{
$Count = $DPs |measure
Write-Host "There are $($Count.count) failed DPs at the moment."
Write-Host "Press any key to redistribute..."
Write-Host ""
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
foreach ($dp in $DPs) {
Write-Host "Redistributing $($DP.PackageID) to $($DP.ServerNALPath.Substring(12,7))...." -ForegroundColor Green
try {

Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_DistributionPoint -Filter "PackageID='$($DP.PackageID)' and ServerNALPath like '%$($DP.ServerNALPath.Substring(12,7))%'" | Set-CimInstance -Property @{RefreshNow = $true}
}
catch {
Write-Host "Failed to redistribute to $($DP.ServerNALPath.Substring(12,7))" -ForegroundColor Red
}

}
Write-Host "Press any key to close window...."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
else
{
Write-Host "There are no Failed DPs" -ForegroundColor Green
Write-Host "Press any key to close..."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

}

Remove-CimSession -CimSession $SiteServerCIM 

Create the following XML-file to enable the right-click menu under Content Status. The file should be placed in the following folder;

14214306-59f0-46cf-b453-a649f2a249e1


<ActionDescription Class="Executable" DisplayName="Redistribute to all failed DPs" MnemonicDisplayName="Redistribute to all failed DPs" Description = "Redistribute to all failed DPs" RibbonDisplayType="TextAndSmallImage">
<ShowOn>
<string>ContextMenu</string>
<string>DefaultHomeTab</string>
</ShowOn>
<Executable>
<FilePath>PowerShell.exe</FilePath>
<Parameters>-Executionpolicy bypass -nologo -WindowStyle normal -command "&amp; 'C:\PowerShellScripts\RedistFailed.ps1' '##SUB:__Server##' '##SUB:__Namespace##' '##SUB:PackageID##'" </Parameters>
</Executable>
</ActionDescription>

To enable a second menu under Distribution Point Configuration Status you can use the following script;

image

image


Param(
[Parameter(Mandatory=$true)]
[String]$SiteSrv,
[Parameter(Mandatory=$false)]
[String]$SiteNamespace,
[Parameter(Mandatory=$True)]
[String]$Server
)

$SiteCode = $SiteNamespace.Substring(14)

Write-Host "Checking" $Server -ForegroundColor red
Write-host "Will check for all failed packages (NOT State 0)"
Write-Host ""

$sOpt = New-CimSessionOption –Protocol DCOM
$SiteServerCIM = New-CimSession -ComputerName $SiteSrv -SessionOption $sOpt
try {
$pkgs =  Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_PackageStatusDistPointsSummarizer -Filter "ServerNALPath like '%$($Server)%' AND (State != '0')"

if ($pkgs -ne $null)
{
$Count = $pkgs |measure
Write-Host "There are $($Count.count) failed packages at the moment."
Write-Host "Press any key to redistribute..."
Write-Host ""
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
foreach ($pkg in $pkgs) {
Write-Host "Redistributing $($pkg.PackageID) to $($Server)...." -ForegroundColor Green
try {
Get-CimInstance -CimSession $SiteServerCIM -Namespace $SiteNamespace -ClassName SMS_DistributionPoint -Filter "PackageID='$($pkg.PackageID)' and ServerNALPath like '%$($Server)%'" | Set-CimInstance -Property @{RefreshNow = $true}
}
catch {
Write-Host "Failed to redistribute to $($pkg.ServerNALPath.Substring(12,7))" -ForegroundColor Red
}

}
Write-Host "Press any key to close window...."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
else
{
Write-Host "There are no Failed pkgs" -ForegroundColor Green
Write-Host "Press any key to close..."
$a = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

}

Remove-CimSession -CimSession $SiteServerCIM

}
Catch {
Write-Error "Failed to query $($Sitesrv)"
Remove-CimSession -CimSession $SiteServerCIM
} 

To enable the right-click menu, create a new XML-file under the following folder;

d8718784-99d5-4449-bc28-a26631fafc07

Content;

<ActionDescription Class="Executable" DisplayName="Redistribute all failed Pkgs" MnemonicDisplayName="Redistribute all failed Pkgs" Description = "Redistribute all failed Pkgs" RibbonDisplayType="TextAndSmallImage">
<ShowOn>
<string>ContextMenu</string>
<string>DefaultHomeTab</string>
</ShowOn>
<Executable>
<FilePath>PowerShell.exe</FilePath>
<Parameters>-Executionpolicy bypass -nologo -WindowStyle normal -command "&amp; 'C:\PowerShellScripts\redistfailedpkgstodp.ps1' '##SUB:__Server##' '##SUB:__Namespace##' '##SUB:NAME##'" </Parameters>
</Executable>
</ActionDescription> 

You can download the scripts from here, but you need to copy the XML-files into the folder of the Admin-Console yourself to make them visible.

Location;

c:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\XmlStorage\Extensions\Actions

Create Application

During a project I created a specific tool to ease the insert of applications within SCCM 2012. It was only tested for SCCM 2012 SP1 CU3 and on Windows 7 x64, with Windows Management Framework 3.0.

Video that demonstrates the tool; Create Application

David O`Brien wrote some great code that sorts out the poor quality of the SCCM 2012 PowerShell support. It is used as part of the script process.

Luca Sturlese made a logging function for PowerShell available which I used to create the log-file.

The idea is that you copy the source-files to your source-folder for all your apps, right-click the MSI and then the script creates an application within SCCM 2012 based on the MSI information. A log-file is generated incase some troubleshooting is necessary, but for status you can simply review the notification bubble.

image

I haven’t tested this on SCCM 2012 R2. I code horribly. I know the installer breaks on a x86 platform. It most likely works on Windows 8+, but I haven’t tested that. The script is depending on multiple WMI-calls, which in high latency environments will cause issues.

You are welcome to try this, send me improvements – or even take this for a spin and just make it better. I was tired of bitcoin casino doing this manually, and copy and pasted something from the internet which solved my problem. Take this for what it is. Nothing more, nothing less.

Some tips for installing the MSI;

Use the following properties;
SITESERVER
FQDN to site-server
BASEFOLDER
Basefolder where all packages will be created. A new sub-folder with username will be created
USEFOLDERS
Base Application Name on MSI-info (false) or folder-name
(true – requires; APPNAME\DEPLOYMENT folder structure)

LOGONREQUIREMENTTYPE
True or False
ALLOWCLIENTSTOUSEFALLBACKSOURCELOCATIONFORCONTENT
True or False
ESTIMATEDINSTALLATIONTIME
Numbers only
FALLBACKTOUNPROTECTEDDP
True or False
MAXIMUMALLOWEDRUNTIMEMINUTES
Numbers only
ONSLOWNETWORKMODE
Download or DoNothing
INSTALLATIONLOG
Path and prefix to where log-files will be located.
ProductName and ProductVersion will be appended
UNINSTALLLOG
Path and prefix to where log-files will be located.
ProductName and ProductVersion will be appended

Logging happens to:
%TEMP%\a_createapp.log

Download the file here

Apply hotfixes during task sequence

If you want to apply hotfixes as part of a task sequence there are of course numerous ways to achieve this. Using PowerShell, MDT 2012 U1 (soon to be 2013??) with SCCM 2012 can make this look a lot prettier with just a few lines of code.

So, lets set this up.

1. First of all we need a script, named hotfixes.ps1. The code looks like this;

$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$folder = $scriptPath
$total = gci $folder *.msu | measure | Select-Object -expand Count
$i = 0
gci $folder *.msu | foreach {
$i++
WUSA ""$_.FullName /quiet /norestart""
Write-Progress -activity "Applying Hotfixes" -status "Installing $_ .. $i of $total" -percentComplete (($i / $total) * 100)
Wait-Process -name wusa
}

2. Collect the hotfixes necessary into a folder. Place the script in the root, and the hotfixes in a sub-folder named “hotfix”. All hotfixes must be of the .MSU-kind.

image

3. Create a package of this to make the files available within the SCCM 2012 infrastructure

image

4. Create a new MDT 2012 Update 1 task sequence. Add the following steps;

image

As you can see below we set our script to be executed and reference the package which contains the updates

image

5. Deploy the task sequence to a computer and verify the results;

image

Looks nice? Well, the script is partly built on the ideas of Niklas Åkerlund, a fellow Swede, on howto install multiple Windows hotfixes. Using a forum-post response by Michael Niehaus I gathered that it would be rather simple to output it more nicely and provide insight into progress using the task sequence progress bar.

Detection logic for MSU in SCCM 2012

I spent the day reading quite a few blog-articles relating to the detection logic for Microsofts new patching format, and howto properly output that into SCCM 2012.

Detection Method for MSU in Applications for SCCM 2012

Deploying the App-V 5.0 Client Using Configuration Manager 2012 SP1

How to create an application for deploying the App-V 5.0 Client with Configmgr 2012

SCCM 2012: Microsoft Update (MSU) als Applikation verteilen

All of the above does have a few issues though. The detection logic isn’t fulproof as a “false” one could easily error out. As I have previously written – the exit-code of a VBScript isn’t relevant when using it for Global  Conditions and the same seems to apply for Detection methods. As noted on Technet (however, for Global Conditions) the output must be through an Echo (or something similar) – which must be something for both a false and a true scenario.

Sample code;

strComputer = "."

Set objWMIService = GetObject("winmgmts:\\" &amp; strComputer &amp; "\root\CIMV2")

Set colItems = objWMIService.ExecQuery("SELECT * FROM Win32_QuickFixEngineering WHERE HotFixID = 'KB2506143'")

If colItems.Count = 0 Then

Wscript.StdOut.Write "False"

Wscript.Quit(0)

Else

Wscript.StdOut.Write "True"

Wscript.Quit(0)

End If