Install-Printer

Code Release

This script is intended to assist in SCCM deployments of optional printers. The intended effect is to allow a user to open Software Center or the SCCM Application Catalog and click an option such as “Map me to XXX Printer”, and have the printer, port, and driver all install automatically.

Parameters

DriverName <Object> The name of the printer driver as defined in the INF file.
DriverInf <Object> Full path to the Driver INF file
DriverPath <Object> Path to the folder containing the driver files. (Default is the INF directory)
PrinterIP <Object> IP of the printer to map
PrinterPort <Object> Printer’s connection port. Default is 9100
PrinterPortName <Object> Name of the printer port. Defaults to “IP_<IP>”
PrinterCaption <Object> Name of the printer
Remove [<SwitchParameter>] Removes the specified printer instead of installing it
DriverOnly [<SwitchParameter>] Only installs the printer driver. Does not attempt to create the port or printer
AllSteps [<SwitchParameter>] Performs Driver install followed by the port and printer mappings. (Default without this and driver only is to only map a printer)
WriteLog [<SwitchParameter>] Writes Debug data to a log file. (Default is true)
RemoveAll [<SwitchParameter>] Removes all mapped printers. (You may need to run this once as the user, and once as an admin for full effect)
AdditionalRemovalExclusions <Object> When used with RemoveAll, prevents the removal of listed printers. By default, Send to One Note, and FAX are excluded.

Usage

PowerShell Script

To add a printer:

&"" -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -PrinterIP "192.168.1.2"

To add a driver:

&"" -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -DriverOnly

To remove a printer and also disable logging

&"" Install-Printer.ps1 -PrinterCaption "My Printer" -Remove -WriteLog:$false

To install a driver and then map a printer (Note that, in general, only administrators have the permissions to successfully use the script this way)

&"" -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -PrinterIP "192.168.1.2" -AllSteps

To remove all printers, or a selection of printers

&"" -RemoveAll -AdditionalRemovalExclusions @("My Printer", "Send by E-mail printer")

SCCM Usage

To install a printer from SCCM, do the following.

  1. Create an application package for your driver. This can be an executable install or an INF install. If you are using an INF file, you may use the Install-Printer script to help.
    Example of the Driver Installation Programs Tab
    1. To create an INF file Application deployment. Create a new application deployment. Set the command to
      &"Install-Printer.ps1" -DriverOnly -DriverName '' -DriverINF ''
      Example of the Driver Installation Detect Tab
      Example of the Driver User Experience Tab

      Example of the Driver Installation Detect Script
    2. For the detection method, us the “Driver Detection” script in the “script” section of this page.
  2. Create an application package for your printer.
    1. Create a new Application Deployment. Set the command to “Install-Printer.ps1” -PrinterCaption ‘My Printer’ -DriverName ‘Xerox 7845 PS v4’ -PrinterIP ‘192.168.1.2’

      Example of the Printer Installation Programs Tab
    2. Set the detection method to the “Printer Detection” script in the “script” section of this page.

      Example of the Printer Installation Detect Script
    3. Set this package to install for the user.

      Example of the Printer Installation User Experience Tab
    4. Add a requirement that points to the appropriate driver application package that you created earlier.

      Example of the Printer Installation Dependency Tab

Script

Install-Printer

<#
.SYNOPSIS
    Provides helpfull script to add printers to a machine
 
.DESCRIPTION
    Provides a method to add a printer, driver, and port to a machine
 
.PARAMETER DriverName
    The name of the printer driver as defined in the INF file.
 
.PARAMETER DriverInf
    Full path to the Driver INF file
 
.PARAMETER DriverPath
    Path to the folder containing the driver files. (Default is the INF directory)
 
.PARAMETER PrinterIP
    IP of the printer to map
 
.PARAMETER PrinterPort
    Printer's connection port. Default is 9100
 
.PARAMETER PrinterPortName
    Name of the printer port. Defaults to "IP_". If you need to map the printer to a default local port, use the name of the port instead. Such as "FILE:".
 
.PARAMETER PrinterCaption
    Name of the printer
 
.PARAMETER Remove
    Removes the specified printer instead of installing it
 
.PARAMETER DriverOnly
    Only installs the printer driver. Does not attempt to create the port or printer
 
.PARAMETER AllSteps
    Performs Driver install followed by the port and printer mappings. (Default without this and driver only is to only map a printer)
 
.PARAMETER WriteLog
    Writes Debug data to a log file. (Default is true)
 
.PARAMETER RemoveAll
    Removes all mapped printers. (You may need to run this once as the user, and once as an admin for full effect)
 
.PARAMETER LocalPrinter
    Specified that the printer is local to the machine. This is needed if mapping a non-network device. (Such as the XPS printer)
 
.PARAMTER PrinterOnly
    Only maps a printer, does not add a port, nor install a driver.
 
.PARAMETER AdditionalRemovalExclusions
    When used with RemoveAll, prevents the removal of listed printers. By default, Send to One Note, and FAX are excluded.
 
.OUTPUTS
    Log file located at C:\Temp\.log
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -PrinterIP "192.168.1.2"
 
.EXAMPLE
    Install-Printer.ps1 -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -DriverOnly
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption "My Printer" -Remove -WriteLog:$false
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption "My Printer" -DriverName "Xerox 7845 PS v4" -DriverInf "\\Drivers\Xerox\7845\Driver.inf" -PrinterIP "192.168.1.2" -AllSteps
 
.EXAMPLE
    Install-Printer.ps1 -RemoveAll -AdditionalRemovalExclusions @("My Printer", "Send by E-mail printer")
 
.EXAMPLE
    Install-Printer.ps1 -PrinterCaption 'Microsoft XPS Document Writer' -PrinterPortName 'FILE:' -DriverName 'Microsoft XPS Document Writer v4' -PrinterOnly -LocalPrinter
 
.NOTES
    Version:        1.3
    Author:         Matthew Thompson
    Creation Date:  2015-12-16
    Purpose/Change: PrinterOnly, and LocalPrinter Features, Printer Removal by IP, Bug fix in event no printers exist and script is run to install one.
    This script was heavily influenced by Kris Powell. (http://www.adminarsenal.com/admin-arsenal-blog/how-to-add-printers-with-powershell)
#>
[CmdletBinding()]
Param
(
    [string]$DriverName, 
    [string]$DriverInf,
    [string]$DriverPath={if ($DriverINF -ne $null){(Split-Path $DriverInf)}}, 
    [string]$PrinterIP, 
    [int]$PrinterPort=9100, 
    [string]$PrinterPortName=("IP_"+$PrinterIP), 
    [string]$PrinterCaption, 
    [Switch]$Remove,
    [switch]$DriverOnly,
    [switch]$AllSteps,
    [switch]$WriteLog=$true,
    [switch]$RemoveAll,
    [string[]]$AdditionalRemovalExclusions=@(),
    [switch]$LocalPrinter,
    [switch]$PrinterOnly
)
 
#Setup the exclusions for RemoveAll
$Exclusions = @("Fax", "Send To OneNote 2013", "Send To OneNote 2010", "Microsoft XPS Document Writer", "Microsoft XPS Document Writer v4")
$Exclusions+=$AdditionalRemovalExclusions
 
$Log=""
 
#Log File Info
$ScriptName = Split-Path -Leaf $PSCommandPath
$LogFile = Join-Path -Path "C:\Windows\Temp" -ChildPath ($ScriptName+".log")
#Do a self-referential lookup to get the script version. As long as the .NOTES section of the file header is up to date and the script is trusted, this should work.
(Get-Help $PSCommandPath -Full -ErrorAction SilentlyContinue).alertset.alert.Text -match "^\s*Version\s*:\s*[\w\d\.\,]*" | Out-Null
if ($matches-ne$null-and$matches.Count-ge1)
{
    $Version = $matches[0] -replace "^\s*Version\s*:\s*",""
}
 
#Write log data
$Log+="Version: "+$Version+"`r`n"
 
#Write Parameters to log
$PSBoundParameters.Keys | ForEach-Object -Process {$Log+=("`""+$_+"`" = `""+$PSBoundParameters[$_]+"`"`r`n")}
 
#Creates a printer object for the user.
function Create-Printer
{
    Param 
    (
        $PrinterCaption, 
        $PrinterPortName, 
        $DriverName,
        [Switch]$Local=$false
    )
    try
    {
        $Printers = @()
        $Printers += Get-WmiObject -Class "Win32_Printer"
        [string]$Log="Installed Printers ("+$Printers.Length.ToString()+") `r`n"
        $Printers | ForEach-Object -Process {$Log+=$_.Name+"`r`n"}
        if ($Printers.Length -eq 0 -or ($Printers.Name).Contains($PrinterCaption) -eq $false)
        {
            $Log+="Attempting to create printer`r`n"
            $Instance = Set-WmiInstance -Class "Win32_Printer" -Arguments @{Caption = $PrinterCaption;DriverName = $DriverName;PortName = $PrinterPortName;DeviceID = $PrinterCaption;Network = (-not $Local)} -ErrorAction Stop
            if ($Instance -ne $null)
            {
                $Log+= "Printer Created`r`n"+$Instance.ToString()+"`r`n"
            }
            else
            {
                $Log+="Install attempted, but printer null`r`n"
            }
        }
        $Log+= "Printer Exists: "+(($Printers.Name).Contains($PrinterCaption)).ToString()+"`r`n"
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
#Installs a driver from an INF file
function Install-Driver
{
    Param
    (
        $DriverName, 
        $DriverPath, 
        $DriverInf
    )
    $Log=""
    try
    {
        $Drivers = @()
        $Drivers += Get-WmiObject -Class "Win32_PrinterDriver"
        if ($Drivers.Length -eq 0 -or -not ($Drivers | Where-Object -FilterScript {$_.Name -like ($DriverName+"*")}))
        {
            $Instance = ([wmiclass]"Win32_PrinterDriver").CreateInstance()
            $Instance.Name = $DriverName
            $Instance.DriverPath = $DriverPath
            $Instance.InfName = $DriverInf
            Invoke-WmiMethod -Class "Win32_PrinterDriver" -Name "AddPrinterDriver" -ArgumentList @($Instance) -Impersonation Impersonate -EnableAllPrivileges | Out-Null
            $Log+= $Instance.ToString()+"`r`n"
        }
        else
        {
            $Log+= ("Driver Exists: "+(($Drivers | Where-Object -FilterScript {$_.Name -like ($DriverName+"*")})).ToString())+"`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
#Creates a printer port required to map a printer
function Create-PrinterPort 
{
    Param
    (
        $PrinterIP, 
        $PrinterPort, 
        $PrinterPortName
    )
    $Log=""
    try
    {
        $PrinterPorts = @()
        $PrinterPorts += Get-WmiObject -Class "Win32_TCPIPPrinterPort"
        if ($PrinterPorts.Length -eq 0 -or -not ($PrinterPorts.Name).Contains($PrinterPortName))
        {
            $Instance = Set-WmiInstance -Class "Win32_TCPIPPrinterPort" -Arguments @{Name = $PrinterPortName;HostAddress = $PrinterIP;PortNumber = $PrinterPort;SNMPEnabled = $false;Protocol = 1}
            $Log+=  $Instance.ToString()+"`r`n"
        }
        else
        {
            $Log+= ("Printer Port Exists: "+(($PrinterPorts.Name).Contains($PrinterPortName)).ToString())+"`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
function Get-PrinterPort
{
    Param($IP)
    $Log=""
    $ToReturn = @()
    try
    {
        $PrinterPorts = @()
        $PrinterPorts += Get-WmiObject -Class "Win32_TCPIPPrinterPort"
        if ($PrinterPorts.Length -eq 0 -or -not ($PrinterPorts.HostAddress).Contains($IP))
        {
            #$Instance = Set-WmiInstance -Class "Win32_TCPIPPrinterPort" -Arguments @{Name = $PrinterPortName;HostAddress = $PrinterIP;PortNumber = $PrinterPort;SNMPEnabled = $false;Protocol = 1}
            $Log+= "No Printer Port exists matching $IP"
        }
        else
        {
            $ToReturn = $PrinterPorts | Where-Object -FilterScript {$_.HostAddress -eq $IP}
            $Log+= ("Printer Port Exists: "+(($PrinterPorts.Name).Contains($PrinterPortName)).ToString())+"`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $ToReturn
}
 
#Deletes an already mapped printer
function Delete-Printer
{
    Param($PrinterName, $IP)
    $Log=""
    try
    {
        $Printers = @()
        $Printers += Get-WmiObject -Class "Win32_Printer"
        if ($PrinterName -ne $null)
        {
            $Printers = $Printers | Where-Object -FilterScript {$_.Name -eq $PrinterName}
        }
        elseif ($IP -ne $null)
        {
            $Ports = Get-PrinterPort -IP $IP
            $NewPrinterList = @()
            foreach ($Port in $Ports)
            {
                $NewPrinterList += $Printers | Where-Object -FilterScript {$_.PortName -eq $Port.Name}
            }
            $Printers = $NewPrinterList
        }
        else
        {
            $Log+="No parameters specified to limit removal. No printers removed"
            return $Log
        }
        foreach($Printer in $Printers)
        {
            $Printer.Delete() | Out-Null
            $Log+= $Printer.Name+" deleted`r`n"
        }
    }
    catch
    {
        $Log+=$_.ToString()+"`r`n"
    }
    return $Log
}
 
#Script Entry
if ($Remove)
{
    Write-Host "Removing $PrinterCaption"
    #Remove a printer selected
    $Log+=Delete-Printer -PrinterName $PrinterCaption -IP $PrinterIP
}
elseif($RemoveAll)
{
    #Remove most printers
    $Printers = @()
    $Printers += Get-WmiObject -Class "Win32_Printer" | Where-Object -FilterScript {$Exclusions.Contains($_.Name) -eq $false}
    if ($Printers.Length -gt 0)
    {
        Write-Host ("Attempting to remove "+$Printers.Length+" printers. You may need to run this script once as admin, and once as the user for all printers to be removed.")
        foreach($Printer in $Printers)
        {
            $Log+=(Delete-Printer -PrinterName $Printer.Name)+"`r`n"
        }
    }
    else
    {
        Write-Host "No printers require removal."
    }
}
else
{
    #Install a printer
    if ($DriverOnly -or $AllSteps)
    {
        #Install the printer Driver
        Write-Host "Installing $DriverName"
        $Log+=(Install-Driver -DriverName $DriverName -DriverPath $DriverPath -DriverInf $DriverInf)+"`r`n"
    }
    if (-not $DriverOnly -and -not $PrinterOnly)
    {
        #Output for user visibility
        Write-Host "Mapping printer ($PrinterCaption) from IP $PrinterIP"
        #Add the printer port
        $Log+=(Create-PrinterPort -PrinterIP $PrinterIP -PrinterPort $PrinterPort -PrinterPortName $PrinterPortName)+"`r`n"
        #Finally, create the printer
        $Log+=(Create-Printer -PrinterCaption $PrinterCaption -PrinterPortName $PrinterPortName -DriverName $DriverName -Local $LocalPrinter)+"`r`n"
    }
    if ($PrinterOnly)
    {
        #Output for user visibility
        Write-Host "Mapping printer ($PrinterCaption) from port $PrinterPortName"
 
        $Log+=(Create-Printer -PrinterCaption $PrinterCaption -PrinterPortName $PrinterPortName -DriverName $DriverName -Local $LocalPrinter)+"`r`n"
    }
}
#Write the log to a file
if ($WriteLog)
{
    $Log | Out-file $LogFile
    Write-Verbose $Log
}

Driver Detection

Edit the “lt;DriverName>” Portion of the script below with the name of your driver. Then add the script into SCCM as your script-based detection method for the INF driver installation.

#Note that this script is not perfect, you may need to change the detection method if you have multiple similiarly named drivers.
$DriverName = "" #Replace this with your driver name
$Drivers = Get-WmiObject -Class "Win32_PrinterDriver"
if (($Drivers | Where-Object -FilterScript {$_.Name -like ($DriverName+"*")}))
{
    Write-Host "Driver already installed"
}
exit 0

Printer Detection

Edit the “<PrinterName>” portion of the script below with your own printer name. Then add this to your printer deployment as the script-based detection method.

$PrinterCaption = "" #Change this to your printer name
$Printers = Get-WmiObject -Class "Win32_Printer"
if (($Printers.Name).Contains($PrinterCaption))
{
    Write-Host "Printer already installed"
}
exit 0

Remove-Printer (Interactive Printer Removal Tool)

This must be placed in the same directory as Install-Printer.ps1 This will present a user with a list of printers to exclude. The user can then select a printer by inputting it’s number. If they do, it will be excluded. Once finished, the script will remove all other printers.

<# .SYNOPSIS Interactively assists a user in removing printers .DESCRIPTION Interactively assists a user in removing printers .NOTES Version: 1.0 Author: Matthew Thompson Creation Date: 2017-01-08 Purpose/Change: Updated for Blog #>
 
#Printers to exclude by default
$Exclusions = @("Fax", "Send To OneNote 2013", "Send To OneNote 2010")
 
#Grab a list of printers
$Printers = Get-WmiObject -Class "Win32_Printer" | Where-Object -FilterScript {$Exclusions.Contains($_.Name) -eq $false}
 
#Stores additional exclusions by user
$AdditionalExclusions=@()
 
#Infinite loop (Breaks out with user interaction)
while($true)
{
    Write-Host "Printers to remove:"
    #Print all non excluded printers for user to browse
    $NonExcluded=@()
    $NonExcluded += $Printers | Where-Object -FilterScript {$AdditionalExclusions.Contains($_.Name) -eq $false}
    for($I=0;$I -lt $NonExcluded.Length;$I++)
    {
        Write-Host ($I.ToString()+") "+$NonExcluded[$I].Name)
    }
    #Ask user to exlude another printer or continue
    $UInput = Read-Host -Prompt "Please select a printer to exclude or type `"f`" to finish"
    if ($UInput.ToLower() -eq "f")
    {
        #On continue, break out of infinite loop
        break;
    }
    #Otherwise matching selected index to printer, and add.
    $UIndex=$null
    if ([int]::TryParse($UInput, [ref]$UIndex) -and $UIndex -ge 0 -and $UIndex -lt $NonExcluded.Length)
    {
        $AdditionalExclusions+=$NonExcluded[$UIndex].Name
    }
    else
    {
        #In case someone can't type a number
        Write-Host "Input could not be parsed, please try again"
    }
}
#Call the parent script with the selected exclusions
Write-Host "Removing printers"
&".\Install-Printer.ps1" -RemoveAll -AdditionalRemovalExclusions $AdditionalExclusions -WriteLog

MTPing

Code Release

MTPing (or Multi-Threaded Ping) is a powershell script designed to quickly ping a large list of machines.

Parameters

MachineList

This parameter should be a string that points to the location of a list of machines. The list should be formatted so that there is one machine per line.
Example:

192.168.1.1
127.0.0.1
www.google.com
microsoft.com
::1

LogLocation

LogLocation should be a string path representing where the log file should be saved. The log file will be written in a simple tabular format. The first column contains the machine-name or ip as retrieved from the machine list. The second column contains the ping result. “True” meaning connection successful.
Example:

192.168.1.1	False
127.0.0.1	True
www.google.com	True
microsoft.com	True
::1	True

Usage

Inside of a powershell console enter the following:

&"" -MachineList "" -LogLocation ""

Example:

&"C:\Scripts\MTPing.ps1" -MachineList "C:\Scripts\Machines.txt" -LogLocation "C:\Logs\PingResults.txt"

Script


Param
(
    [Parameter(Mandatory=$True)]
    $MachineList,
    [Parameter(Mandatory=$True)]
    $LogLocation
)
 
$ToProcess = @()
$StreamReader = [System.IO.StreamReader] $MachineList
while (-not $StreamReader.EndOfStream)
{
    $ToProcess+=$StreamReader.ReadLine();
}
$StreamReader.Close();
 
$ScriptBlock = 
{
    param
    (
        [PSObject]$InputObject
    )
    return @((Test-Connection $InputObject -quiet), $InputObject)
}
 
$Stream = [System.IO.StreamWriter] $LogLocation
$Pool = [RunspaceFactory]::CreateRunspacePool(1, 10)
$Pool.ApartmentState = "STA"
$Pool.Open()
$Pipes = @()
 
foreach($Object in $ToProcess)
{
    $PipeLine = [System.Management.Automation.PowerShell]::create()
    $PipeLine.RunspacePool = $Pool
    $Mute=$PipeLine.AddScript($ScriptBlock).AddArgument($Object)
    $Pipes+= @{
                Pipe = $PipeLine
                AsyncHandle = $PipeLine.BeginInvoke()
              }
}
 
$Complete=$false;
$Completed=0;
while(-not $Complete)
{
    $Found = $false;
    for ($Index=0; $Index -lt $Pipes.Count; $Index++)
    {
        if ($Pipes[$Index] -ne $null)
        {
            $Found=$true;
            if ($Pipes[$Index].AsyncHandle.IsCompleted)
            {
                $Result = $Pipes[$Index].Pipe.EndInvoke($Pipes[$Index].AsyncHandle)
                $Pipes[$Index].Pipe.Dispose()
 
                if ($Result[0])
                {
                    Write-Host $Result[1] -ForegroundColor Green -NoNewline
                    Write-Host "`t"($Completed/$Pipes.Count*100)"%"
                }
                else
                {
                    Write-Host $Result[1] -ForegroundColor Red -NoNewline
                    Write-Host "`t"($Completed/$Pipes.Count*100)"%"
 
                }
 
                $Stream.WriteLine($Result[1]+"`t"+$Result[0].ToString());
                $Pipes[$Index] = $null;
                $Completed++;
            }
        }
    }
    if (-not $Found)
    {
        $Complete = $true;
    }
}
$Stream.Close();
$Pool.Close();

HashCompare

Code Release

HashCompare compares a file with a known MD5 hash and returns the output in an SCCM compatible manner. To understand more about why this script exits as it does, you should read “To use a custom script to determine the presence of a deployment type” (Under step 4) on technet.

Usage

Configuration

The script is fairly simple. You only need to configure the top 2 lines. First set $FilePath to the location of the pre-existing file. Then set $ExpectedHash to the MD5 hash of the deployed file. You can get the MD5 hash from powershell using Get-Hash in Powershell 4.0 or, if you are not up to 4.0, by using this Get-Hash.

General Usage

Inside of a powershell console enter the following:

&""

Example:

&"C:\Scripts\HashDetect.ps1"

SCCM Usage

After configuration, import the script as a new Detection Method for your deployment.

Script


$FilePath = "C:\ProgramData\MyApp\MySettings.xml"
$ExpectedHash = "00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00"

if (-not (Test-Path $FilePath))
{
    #File path not found. Not installed.
    exit 0
}

try
{
    $FileStream = New-Object IO.FileStream $FilePath, "Open"
    $MD5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
    $Hash = [System.BitConverter]::ToString($MD5.ComputeHash($FileStream))
}
catch
{
    Write-Error $_.ToString()
    exit 1
}

#Compare the hash with a known hash
if ($Hash -eq $ExpectedHash)
{
    Write-Output "Hash match. File already installed."
    exit 0
}
else
{
    #Hash MisMatch. Not installed
    exit 0
}

Get-Hash

Quick code release

Get-Hash returns the MD5 or SHA256 hash of a file. (Note: Powershell 4.0 has a built-in command for this.)

Parameters

FilePath

The path to the file you wish to hash.

Usage

Inside of a powershell console enter the following:

"" -FilePath ""

Example:

&"C:\Scripts\Get-Hash.ps1" -FilePath "C:\FileToHash.xml"

Script

MD5


Param($FilePath)
 
if (-not (Test-Path $FilePath))
{
    #File path not found. Not installed.
    Write-Error "File does not exist"
}else
{
    try
    {
        $FileStream = New-Object IO.FileStream $FilePath, "Open"
        $MD5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
        $Hash = [System.BitConverter]::ToString($MD5.ComputeHash($FileStream))
        Write-Output $Hash
    }
    catch
    {
        Write-Error $_.ToString()
    }
}

SHA256


Param($FilePath)
 
if (-not (Test-Path $FilePath))
{
    #File path not found. Not installed.
    Write-Error "File does not exist"
}else
{
    try
    {
        $FileStream = New-Object IO.FileStream $FilePath, "Open"
        $SHA2 = new-object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider
        $Hash = [System.BitConverter]::ToString($SHA2.ComputeHash($FileStream))
        Write-Output $Hash
    }
    catch
    {
        Write-Error $_.ToString()
    }
}

BGPSInfo

Release Announcement

BGPSInfo is my attempt to replace BGInfo with something customizable. Powershell was chosen to accomplish this. The script has only had limited testing so far. Use at your own risk.

You may download the PowerShell script: GitHub

Skype Won’t Login

This post was migrated from my older blog.

We had an issue a while ago where Skype would not sign several users in. There was nothing obvious from Skype itself, no errors, no timeouts. It just sat there spinning. I opened up WireShark and found no packets were even being attempted from Skype to our server. In procmon, I similarly found nothing happening after the logon is attempted, no errors, no network traffic, no access denied, etc. A coworker finally managed to get some headway by noticing that all affected users had no network connections shown in the Network and Sharing center. This was odd as they did have internet access; could check e-mail, browse the internet, access domain resources, etc. It turns out, the cause was permissions on a specific set of registry keys dealing with network information. Specifically the netprofm account did not have access to the following registry keys

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles

Once the permissions were corrected, Network and Sharing center showed our domain connection and Skype started functioning, didn’t even require a reboot.

Installing Java through SCCM

This post was migrated from my older blog.

I have been trying to get Java to install smoothly from SCCM for quite a while now. We need to deploy Java in a way that ensures that old versions of Java are removed, and both 32-bit and 64-bit versions of Java are installed. Here is a list of problems a ran into, as well as fixes for each.

The GUID is only Semi-Predictable

Uninstalling Java improves security as the Java installer frequently fails to remove previous versions. So, how do we uninstall Java remotely through SCCM? The most obvious approach is to remove Java by the GUID with MSIEXEC. Example:

MsiExec.exe /X{26A24AE4-039D-4CA4-87B4-2F06417067FF} /qn /norestart

This works well, but you have to know the GUIDs beforehand. If you search for the Java GUIDs you will likely find forums with comments stating to just replace parts of the GUID with the software version and bit version. We had a script that, mostly, worked based on this concept.

Here is the general suggestion you will find when you search for the Java GUIDs. The first bolded part of the following GUID is the bit version of Java, 32 or 64. The second bolded part relates to the specific version of Java. In this case, 7 update 55.

{26A24AE4-039D-4CA4-87B4-2F83217055FF}

This does not work all the time however.

Here’s an example:

Java 7 Update 55 {26A24AE4-039D-4CA4-87B4-2F83217055FF}
Java 7 Update 60 {26A24AE4-039D-4CA4-87B4-2F03217060FF}

The bolded parts changed outside the expected pattern.

That’s just one example, it seems as though subtle changes like that are littered through the Java releases. Most scripts that remove multiple versions of Java don’t seem to account for this. Our in-house script didn’t.

I noticed that the parts of the GUID that frequently change are only on the last segment of the GUID. The front segments of the GUID rarely change. I built a powershell script to remove Java on this concept.

Click here to go to the wiki page on the JavaRemoval script.

SCCM starts 32-bit Powershell by default

The removal works well by itself, but had issues when used in an SCCM package. It only removed 32-bit versions of Java. This leads to multiple 64-bit versions of Java. Thanks to the logging lines I added to the script I found out that it was only receiving the 32-bit keys, even from the 64-bit registry path. Turns out, SCCM executes the script with the 32-bit Powershell. This was a quick fix thanks to this technet post. I took jfvanroy’s answer and entered in the Powershell script name. It is working well so far. You can find a copy of it below.

If "%PROCESSOR_ARCHITEW6432%"=="" GOTO Native
%windir%\Sysnative\windowsPowershell\V1.0\PowerShell.exe -NoProfile -ExecutionPolicy Bypass -file %~dp0JavaRemoval.ps1
GOTO END
:Native
PowerShell.exe -NoProfile -ExecutionPolicy Bypass -file %~dp0JavaRemoval.ps1
:END

The Uninstall Restart

The next problem I ran into with uninstalling/installing Java is a reboot requirement. Our original batch file uninstalled and installed Java in one run. This was hit or miss for 2 reasons. The first script that we made did not have a /norestart switch to the uninstall command. When a restart was required, the machine rebooted. The problem with that is it cancels the rest of the script which prevents Java from being installed. SCCM does not resume scripts that restart in the middle of their execution. As for the second reason; when we added the /norestart switch the install fails due to a pending restart. One of the main reasons a reboot was required is because IE or Java was still in use by a user at the deployment deadline.

We originally remedied these issues by using a task-sequence with a required reboot in-between an uninstall and install step. We added a few taskkill commands to our script to help with the program in-use issues. Not exactly elegant, but it worked… mostly. This was before we figured out the GUID issues. We are now using a single application package for uninstall and 2 application packages for install. This is done to bridge any gaps created by the install. For example, if you uninstall Java, then install the latest version, this causes a gap in the Java coverage for the user. To fix this, deploy two packages, x86 and x64, of Java. Then about a week later, deploy an uninstall package the removes all versions except for the version you want to standardize your users on.

The Installer doesn’t work with the 64-bit System Account

When you use the executable installer to install Java, it extracts an MSI and CAB file to the AppData folder. It then runs a MSIEXEC /i command to install the extracted MSI file. For home users this usually works, as it uses the home user’s account. SCCM, however, uses the system account. The system account is a little odd in that, it has two profiles. A 32-bit and a 64-bit profile. MSIEXEC is a 64-bit utility in 64-bit versions of windows, but the Java installer runs as 32-bit. The files that the installer extracts ends up in the 32-bit system profile. When MSIEXEC goes to install the MSI, it looks in the 64-bit profile and fails to find it.

This is a known bug and has been for a while. Shavlik submitted a bug report to Java back in 2010 on the issue, however it was recently resolved with the resolution “Won’t Fix”.

A workaround to this, is to extract the MSI, and CAB file for older versions, and run those directly. It works but it is an unsupported practice according to Oracle. Update: You were once able to find instructions on how to do that at Oracle’s website. This page now advertises their enterprise “My Oracle Support” option. As such, I will include the instructions below.

  1. Download the Java SE JRE Offline installers from the Java SE Downloads page
  2. Run the installers but do not click next or continue the installation!
  3. Once the installer is visible, navigate to “C:\Users\[Your UserName]\AppData\LocalLow\Oracle\Java” or “%APPDATA%\..\LocaLow\Oracle\Java”. Inside this directory you should see a folder matching the version of Java you started the installer for. For example “jre1.8.0_112_x64”. Copy everything inside the folder to wherever you need the install files to be. Pre Java 8 Update 20, this folder usually contained an MSI and CAB file. Afterwards, it is usually just a single MSI file. Either way, that MSI file and any supporting files in that folder make up the extracted installer. Repeat this as needed for the 32/64 bit versions if you are installing those as well.

Java extracts two files (Pre 8 Update 20)

I briefly attempted to deploy Java through SCUP. I sent a test deployment out and it half worked. Some received the update, some didn’t. Mostly it was just a mess. At the time I was not fully sure why, but it is likely that it was due to the same issue I had deploying Java as an application package. It extracted to the wrong system profile

Before entirely giving up on the idea, I decided to see if I could send it out using the extracted MSI files. It was not even plausible as a concept as Java extracts to 2 files, a MSI and a CAB. Both files are required for the install to succeed. SCUP does not support multi-file installers unfortunately.

Update 2016-01-07: The 2 file issue appears to have stopped. Extracting Java now only seems to produce a single MSI file. I have not attempted a SCUP deployment however. If it does work, the next problem will still be an issue.

Java doesn’t remove old versions in silent mode

Java recently, starting with an update of 8, started to prompt during install to remove previous version of Java. This does works well for home users, but for enterprise environments, this does not work in silent mode. There are currently no switches you can use to get the MSI to remove old versions.

Java doesn’t install in silent mode correctly without a config file

Another recent development with Java, is that it now appears to require a config file in order to install in silent mode correctly. The /q switch for msiexec is no longer enough.

The config file needs to be saved to “C:\ProgramData\Oracle\Java\java.settings.cfg”. The file is a plain text file with install commands on every line. The line we need to ensure a functional silent install is “INSTALL_SILENT=1”. See below for an example config file.

AUTO_UPDATE=0
EULA=0
INSTALL_SILENT=1
SPONSORS=0
WEB_ANALYTICS=0

There is a sub issue related to this. If you leave the config file in place after the install occurs, then any administrator that then needs to manually update or remove Java in the future will not see their installer/uninstaller run. More than likely, they will troubleshoot their “failing” installer until they finally realize that the installer actually is running, but is forced into silent mode by the left over file.

My Complete Solution

I have a 3 Deployment solution that appears to work solidly in SCCM. Instructions on how to create the solution is located at Java_Install_Package

In general though, the package can be built as such:

  1. Create an application package. Ensure the source folder contains the extracted installers for whatever version of Java you are installing.
  2. Add to the source folder a silent install config file, and a batch file that installs the config file, installs java, then removes the config file.
  3. Repeat for any other versions of Java.
  4. Use the JavaRemoval script and the batch code from Problem 2 to create a Java Removal package that removes all versions of Java except the ones that you just built packages for.
  5. Deploy the 2 installers for x86 and x64 in SCCM.
  6. Deploy the uninstaller about a week later when 95% or more of your clients have your new version of Java.

The end effect of all this is, when the install program is deployed it will install Java. The uninstall package which should be deployed a week later will uninstall un-needed versions of Java.

Win32Exceptions

This post was migrated from my older blog.

There have been a few times where I found myself starting at an error prompt stating Error: 0x000006C1 or some other unhelpful hex number. Then upon searching for the error code, only finding results for a specific instance of the error for an unrelated application with generic fixes that still don’t hint at the root issue. I found that you can translate some of these error codes using .Net and by extension PowerShell. If you find yourself in this situation, try the following command:

new-object System.ComponentModel.Win32Exception @($ErrorCode);

Where “$ErrorCode” is your error. Example:

new-object System.ComponentModel.Win32Exception @(0x80190193); #Can use integers or hex numbers

I also generated a list of the exceptions here: Win32Exceptions List If the list does not contain your error code, then it is likely just “Unknown error”… not very helpful.

Alternatively, if you utilize SCCM and have the CMTrace tool, you can preform error code lookups directly from the CMTrace application.