Get all properties from MSI installer files

windowsinstallericonThere are times when you need to specify custom values for MSI properties.  If the MSI is from a third party or you do not have the original installer code, it would be nice to be able to get a full listing of all properties exposed by an MSI.  This PowerShell function uses the COM-based WindowsInstaller API to extract properties from 1 to n MSIs.

 

Once you find the properties you want to provide custom values for, you can then execute msiexec.exe using the custom property values:

C:\> msiexec.exe /i [PATH_TO_MY_MSI]  PROPERTY1=[MY_CUSTOM_VALUE1] PROPERTY2=[MY_CUSTOM_VALUE2]

function Get-MsiInfo {
    param(
        [parameter(Mandatory=$True, ValueFromPipeline=$true)]
        [IO.FileInfo[]]$Path,
        [AllowEmptyString()]
        [AllowNull()]
        [string[]]$Property
)
<#
            .SYNOPSIS
            Queries parameter information from one or more MSI files

            .DESCRIPTION
            By default will return the ProductCode,ProductVersion,ProductName,Manufacturer,ProductLanguage,FullVersion.  If an empty string
            is provided for the Property parameter, then all properties are returned

            .PARAMETER Path
            MSI Path(s) provided either explicitly or from the pipeline

            .PARAMETER Property
            The names of the MSI properties to return.  Specify empty string to return all properties

            .EXAMPLE
            gci *.msi | Get-MsiInfo -Property 'ProductName','ProductVersion','Manufacturer'

            --------------------

            Gets specific properties for all MSIs in the current directory

            .EXAMPLE
            gci *.msi | Get-MsiInfo

            --------------------

            Get all properties for all MSIs in the current directory
        #>
    Begin {
        $winInstaller = New-Object -ComObject WindowsInstaller.Installer
    }
    Process {
        try {
            $msiDb = $winInstaller.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $winInstaller, @($Path.FullName, 0))
            if($Property) {
                $propQuery = 'WHERE ' + (($Property | ForEach-Object { "Property = '$($_)'"}) -join ' OR ')
            }
            $query = ("SELECT Property,Value FROM Property {0}" -f ($propQuery))

            $view = $msiDb.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $msiDb, ($query))
            $null = $view.GetType().InvokeMember('Execute', 'InvokeMethod', $null, $view, $null)

            $msiInfo = [PSCustomObject]@{'File' = $Path}
            do {
                $null = $view.GetType().InvokeMember('ColumnInfo', 'GetProperty', $null, $view, 0)
                $record = $view.GetType().InvokeMember('Fetch', 'InvokeMethod', $null, $view, $null)
                if(-not $record) { break; }
                $propName = $record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, 1) | select -First 1
                $value = $record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, 2) | select -First 1
                $msiInfo = $msiInfo | Add-Member -MemberType NoteProperty -Name $propName -Value $value -PassThru
            } while ($true)

            $null = $msiDb.GetType().InvokeMember('Commit', 'InvokeMethod', $null, $msiDb, $null)
            $null = $view.GetType().InvokeMember('Close', 'InvokeMethod', $null, $view, $null)           

            $msiInfo
        }
        catch {
            Write-Warning -Message $_
            Write-Warning -Message $_.ScriptStackTrace

        }
    }
    End {
        try {
            $null = [Runtime.Interopservices.Marshal]::ReleaseComObject($winInstaller)
            [GC]::Collect()
        } catch {
            Write-Warning -Message 'Failed to release Windows Installer COM reference'
            Write-Warning -Message $_
        }

    }
}
Advertisements

Parallel copy files over remoting session

metro-powershell-logoA fairly straightforward method to copy files via PowerShell remote sessions to multiple computers in parallel.

$ErrorActionPreference = 'stop'

workflow Copy-Parallel {
    param ([string[]]$ComputerName, [string]$SourcePath,[string]$TargetPath)
    foreach -parallel ($computer in $ComputerName) {
        InlineScript  {
            try {
                $s = New-PSSession -ComputerName $using:computer
                Copy-Item -Path $using:SourcePath -Destination (Split-Path $using:TargetPath -Parent) -ToSession $s
            } finally {
                Remove-PSSession -Session $s
            }
        }
    }
}

function New-BigFile {
    param([string]$Path,
        [int]$SizeInBytes = 30MB
    )
    $bytes = new-object byte[] 1000000
    try {
        $fs = [io.file]::Create($filePath)
        $bytesWritten = 0
        do {
            $fs.Write($bytes, 0, $bytes.Count)
            $bytesWritten += $bytes.Count
        } while($bytesWritten -lt $SizeInBytes)
    } finally {
        $fs.Close()
        $fs.Dispose()
    }
}

$filePath = 'c:\temp\bigfile.bin'
New-BigFile -Path $filePath -SizeInBytes 100MB
Copy-Parallel -ComputerName dev-bldserv2,dev-bldagnt4,dev-bldagnt5,devbuild8 -SourcePath $filePath -TargetPath $filePath

Richard Siddaway's Blog

In response to this post – https://richardspowershellblog.wordpress.com/2015/05/28/copy-files-over-ps-remoting-sessions/

I was asked how you could copy files in parallel to multiple machines.

As soon as anyone mentions parallelism I think of workflows so I ended up with this

$computers = ‘W16TP5TGT01’, ‘W16TP5TGT02’

workflow parallelcopy {
param ([string[]]$computername)
foreach -parallel ($computer in $computername) {
InlineScript {
$s = New-PSSession -ComputerName $using:computer
Copy-Item -Path C:ScriptsNew-NanoMachine.ps1 -Destination C:Source -ToSession $s
Remove-PSSession -Session $s
}
}
}

parallelcopy -computername $computers

A couple of issues I found. First off –Tosession and –FromSession haven’t been added to the Copy-Item workflow activity. This means you have to use an Inline script block to access those parameters

Secondly accessing a emoting session created outside of the workflow generates a session busy error when trying to perform the copy so have to move the session creation into the Inline script.

If you had a lot of files, or a lot of…

View original post 26 more words

Replacing the Default PSRepository with your own (For PowerShell v 5.0.10586.117)

 

The script below is a template for changing the default PSRepository location.


#Script to change the default PSGallery URLs to custom ones (Needed for PowerShell 5.0.10586.117)
$xmlPath = "${env:APPDATA}\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml" -ireplace "Roaming","Local"

$config = Import-Clixml -Path $xmlPath

$config.Item("PSGallery").SourceLocation = '<YOUR_INTERNAL_PROGET_URL_HERE>'
$config.Item("PSGallery").PublishLocation = '<YOUR_INTERNAL_PROGET_URL_HERE>'
$config.Item("PSGallery").ScriptSourceLocation = 'https://www.powershellgallery.com/api/v2/items/psscript/'
$config.Item("PSGallery").ScriptPublishLocation = 'https://www.powershellgallery.com/api/v2/package/'
$config.Item("PSGallery").Trusted = $true
$config.Item("PSGallery").Registered = $true
$config.Item("PSGallery").InstallationPolicy = 'Trusted'
$config.Item("PSGallery").PackageManagementProvider = 'NuGet'
$config.Item("PSGallery").ProviderOptions = $config.Item("PSGallery").ProviderOptions

Export-Clixml -Path $xmlPath -InputObject $config

Thanks to Richard for his deep research on this issue …

Using Agent Ransack, I found that the repository data is stored in “c:\Users\\AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml”

Richard Siddaway's Blog

In this post – https://richardspowershellblog.wordpress.com/2016/09/17/powershell-repositories/ – I stated that you could unregister the default PowerShell repository. I also said that the statement in the documentation for Unregister-PSrepository that you couldn’t unregister PSGallery was incorrect.

A couple of readers have left comments stating that they tried it and got an error message stating that PSGallery can’t be unregistered.

I did my first test on Windows 10 latest preview build – build 14926

PS> $PSVersionTable

Name Value
—- —–
PSVersion 5.1.14926.1000
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
BuildVersion 10.0.14926.1000
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1

I’ve just test on Windows Server 2016 TP5

PS C:Windowssystem32> $PSVersionTable

Name Value
—- —–
PSVersion 5.1.14300.1000
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
CLRVersion 4.0.30319.42000
BuildVersion 10.0.14300.1000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1

The version of PowerShell 5.1 on Server 2016 TP5 doesn’t have the –Default parameter on Register-PSrepositiry but this works

Register-PSRepository…

View original post 25 more words

Lightweight PowerShell Templating Engine

<#

.SYNOPSIS

Expand-Template.ps1

Simple templating engine to expand a given template text containing PowerShell expressions.

.PARAMETER Text

The text of the template to do the expansion

.PARAMETER Path

Path to template to do the expansion

.PARAMETER Desitination

Destination path to write expansion result.  If not specified, write to output stream.

.PARAMETER PsConfigurationPath                          

Path to file containing PowerShell code. This file could contain variable values to use when evaluating templates

.PARAMETER BeginTag

Begin tag for detecting expand expression in template

.PARAMETER EndTag

End tag for detecting expand expression in template

.EXAMPLE

$message=”hello”; .\Expand-Template.ps1 -Text ‘I would like to say [[$message]] to the world’

#>

[CmdletBinding()]

param

(

    [Parameter(Mandatory=$true, ParameterSetName=‘Text’,ValueFromPipeline=$true)]

    [string]$Text,

    [Parameter(Mandatory=$true, ParameterSetName=‘Path’)]

    [string]$Path,

    [string]$Destination,

    [string]$PsConfigurationPath,

    [string]$BeginTag = ‘[[‘,

    [string]$EndTag = ‘]]’

)

$BeginTag = [RegEx]::Escape($BeginTag)

$EndTag = [RegEx]::Escape($EndTag)

Write-Verbose ‘Expand-Template’

if ($Path) {

    if (!(Test-Path -Path $path)) { throw “Template-Expand: path `’$path`’ can’t be found”  }

    $Text = Get-Content -Path $path -Raw

}

if ($PsConfigurationPath) {

    if (!(Test-Path -Path $PsConfigurationPath)) { throw “Template-Expand: psConfigurationPath `’$psConfigurationPath`’ can’t be found” }

    Write-Verbose ” ** Loading PS Configuration file: $PsConfigurationPath

    . $PsConfigurationPath

}

$pattern = New-Object -Type System.Text.RegularExpressions.Regex `

                      -ArgumentList $BeginTag(.*?)$EndTag,([System.Text.RegularExpressions.RegexOptions]::Singleline)

$matchEvaluatorDelegate =  [System.Text.RegularExpressions.MatchEvaluator] {

           param([System.Text.RegularExpressions.Match]$Match)

           $expression = $match.get_Groups()[1].Value # content between markers

           Write-Verbose ”  ** expanding expression: $expression

           trap { Write-Error “Failed to expand template. Can’t evaluate expression ‘$expression‘. The following error occured: $_; break }

           Invoke-Expression -command $expression | Tee-Object -Variable result

           Write-Verbose ”  ** expanded expression evaluated value:`n$result`n”

    }

$expandedText = $pattern.Replace($text, $matchEvaluatorDelegate)

if (-not $destination){ $expandedText }

else { Set-Content -Path $destination -value $expandedText -encoding $Encoding }

Fastest method to get MSMQ message counts

Getting MSMQ message counts can be painfully slow if there are large numbers of messages in a given queue. This PowerShell script is the fastes known way I have found to get the message counts of a queue. The time to get the counts is linear regardless of the number of messages.


<#
.SYNOPSIS
Gets all queues' information from the specified computer

.DESCRIPTION
Gets all queues' information from the specified computer

Adapted from http://www.codeproject.com/Articles/346575/Message-Queue-Counting-Comparisions

.EXAMPLE
Get-MessageQueueInfo.ps1 -MachineName rg-p-blu2 | sort -Property name| ft -Property Name,MessagesInQueue,BytesInQueue -AutoSize
#>

param([string] $MachineName = $env:COMPUTERNAME)
$path = "\\{0}\root\CIMv2" -f $MachineName
$scope = new-object System.Management.ManagementScope ($path)
$scope.Connect()
$QueryString = "SELECT * FROM Win32_PerfFormattedData_msmq_MSMQQueue"
$query = new-object System.Management.ObjectQuery ($queryString)
$searcher = new-object System.Management.ManagementObjectSearcher ($scope, $query)
$searcher.Get()



Kill a process that is listening on a network port

Sometimes, you want to forcibly deploy/install a piece of software that requires a particular service port. This script will find any process that is listening on that port and kill it and if it is a windows service, also disable the service.

param([string]$Port = '80')

$a = netstat -a -n -o

$a -split [environment]::NewLine | foreach {
    $parts = $_.Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries) 
    $isMatch = $parts | where {$_ -like "*:$Port*" }
    if($isMatch -and $parts[3] -like 'LISTENING') {
        $proc = Get-Process -Id $parts[4] -ErrorAction SilentlyContinue
        if(-not $proc) {continue}
        Write-Warning "A process was found listening on port $($parts[4])"
        $foundService = (Get-WmiObject Win32_Service -Filter "ProcessId='$($parts[4])'") 
        if($foundService) {
            Write-Host "The process was running as a service, the service will be stopped and disabled ($($foundService.PathName))"
            [void]$foundService.StopService()
            [void]$foundService.ChangeStartMode("disabled")
        }

        Write-Host "Killing process $($proc.Name)"
        kill -Id $parts[4] -Force
        $s = Get-Service
    }
}