uninstall installed programs using powershell using partial match - powershell

I have some programs installed in Windows 10 and 7 that start with (in Uninstall Programs)
"Python info.data-5.332234" "Python delta.ind-5.332234" "Python
module.data-15.332234" "Python hatch.back-0.332234"
I've tried various scripts to try and uninstall using partial match with PowerShell, but none of them seems to uninstall the programs.
This is the latest script I've used and does not work... it uninstalls the registry entry but not actually remove the folder or the entry from Uninstall Programs
$remove = #('Python info.data', 'Python delta.ind', 'Python module.data', 'Python hatch.back')
foreach ($prog_name in $remove) {
Write "Uninstalling" % $prog_name
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match $prog_name } | select UninstallString
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
Write "Uninstalling..."
Write $uninstall32
start-process "msiexec.exe" -arg "/X $uninstall32 /qn" -Wait}
}

The Problem is, that the variable $uninstall32 can contain more than 1 entry.
Add a $uninstall32.GetType() before you start msiexec to check if the variable may contains more than one string. If so, msiexec wont run because you are passing two GUIDs at once.
Use the Win32_Product WMI Class to get the GUIDs of the desired Applications.
$remove = #('Python info.data', 'Python delta.ind', 'Python module.data', 'Python hatch.back')
foreach ($prog_name in $remove) {
$uninstall32 = #(Get-WmiObject -Query "SELECT * FROM Win32_Product WHERE Name like '$prog_name%'")
foreach ($product in $uninstall32) {
Write "Uninstalling...`n$($product.Name)"
$exitCode = (Start-Process "msiexec.exe" -ArgumentList "/X $($product.IdentifyingNumber) /qn" -Wait -PassThru).ExitCode
Write "$($product.Name) return ExitCode: $exitCode"
}
}
Also, add the -PassThrue Switch at the Start-Process CMDLet and catch/output the ExitCode of each Uninstallation Process.
Make sure your Powershell/ISE is running with elevated privileges.

Related

PowerShell: What is the best method for parallel execution of commands with logging

I have a vendor provided application that needs to be run once with each of several locally stored configuration files. I have tried two different methods to run these commands in parallel, one is slower but seems to work fine, the second seems faster but I get no logs so I can't confirm that the application is actually being executed.
This first method is definitely running parallel. It is much faster than the old procedural version. So that is great.:
$configList = $(Get-ChildItem E:\Path\*config.ps1 -recurse).FullName
$configList | ForEach-Object -Parallel {
# Get the config and logfile path for each
. $_
Invoke-expression "& E:\dir\app.exe $config" | Out-File -Append -FilePath $logfile
}
From my reading I understand that using a RunspacePool can be even faster, and this does execute faster, but it is producing no entries in my logs so I can't ensure it is running properly. Any help is appreciated.
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
$Jobs = #()
$configList = $(Get-ChildItem E:\Path\*config.ps1 -recurse).FullName
$configList | ForEach-Object {
# Get the config and logfile path for each
. $_
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript({Invoke-expression "& E:\dir\app.exe $config" | Out-File -Append -FilePath $logfile})
$Jobs += $PowerShell.BeginInvoke()
}

Using uninstallstring through PowerShell to uninstall program

The issue I'm having right now is that when I go to run the code through PowerShell, it is changing the value of the uninstall string and adding the variable name before it. The result i'm hoping for is this.
MsiExec.exe /X{2C5B24AD-9F13-52A1-KA2N-8K4A41DC9L4G}
But the result I'm getting from the variable after replacing the /I with an /X and doing .Trim() is the following:
#{UninstallString=/X{2C5B24AD-9F13-52A1-KA2N-8K4A41DC9L4G}}
So I was wondering if you guys would be able to tell me from my code below where I'm going wrong.
I have to replace the /I with /X, because the uninstall string first comes back like this MsiExec.exe /I{2C5B24AD-9F13-52A1-KA2N-8K4A41DC9L4G}, and I'm trying to uninstall, not install.
if ($Uninstall_str) {
#run uninstall here
try {
$Uninstall_str = $Uninstall_str -replace 'MsiExec.exe /I', '/X'
$Uninstall_str = $Uninstall_str.Trim()
Start-Process "msiexec.exe" -Arg "$Uninstall_str /qb" -Wait
} catch {
Write-Output $_.Exception.Message
Write-Output $_.Exception.ItemName
Write-Warning "Error unintalling."
}
}
You didn't expand the UninstallString value when reading it from the registry. Your code for doing that probably looks somewhat like this:
$Uninstall_str = Get-ItemProperty 'HKLM:\...\Uninstall\Something' |
Select-Object UninstallString
Replace that with
$Uninstall_str = Get-ItemProperty 'HKLM:\...\Uninstall\Something' |
Select-Object -Expand UninstallString
and the problem will disappear.
To get rid of the #{uninstallstring all I needed to do was specify what I was trimming on this line
$Uninstall_str = $Uninstall_str.Trim()
So that line changed to the following to receive the results I was looking for.
$Uninstall_str = $Uninstall_str.Trim("#{UninstallString=")

Capture cmd error to file in CURRENT directory?

I have a PowerShell script that restarts servers listed in a file.
foreach ($server in $servers) {
try {
cmd /c "shutdown.exe /r /f /m \\$server /t 0 /d p:0:0 /c 'PlannedRestart'"
"$server`tRestarted"
"$server`tRestarted" | Out-File -FilePath .\RestartServers_LOG.txt -Append
} catch {
"$server`t" + cmd /c ">&1"
"$server`t" + cmd /c "dir > .\RestartServers_LOG.txt 2>&1"
}
}
I am trying to catch any errors that may occur from the CMD to output on the PowerShell session as well as to a file in the CURRENT directory, so in PowerShell that would be .\ but I don't know what the current directory specification is for CMD. Unless its the same thing?
Essentially, I am hoping to accomplish something similar to how we can capture the exception message of PowerShell like this:
"$server`t" + $_.Exception.Message.ToString().Split(":")[1].Replace("`n","") |
Out-File -FilePath .\RestartServers_LOG.txt -Append
but CMD doesn't deal with exceptions like PowerShell, and instead STDERR.
Note: I am using the command shutdown.exe because PowerShell doesn't have ability to restart "planned" or "unplanned" unfortunately.
Just do it the PoSh way:
$params = '/r', '/f',
'/t', '0',
'/d', 'p:0:0',
'/c', 'PlannedRestart'
$servers | ForEach-Object {
$output = & shutdown.exe /m "\\${_}" #params 2>&1
if ($LastExitCode -eq 0) {
"{0}`tRestarted" -f $_
} else {
"{0}`tRestart failed:`t{1}" -f $_, $output
}
} | Set-Content '.\RestartServers_LOG.txt'
External commands don't throw exceptions in the first place, so your try..catch wouldn't do anything anyway.

How do I set up autocomplete in powershell 2.0?

I have a drop down menu that's populated by a CSV file. I had to change my code around to make it work with v2. The last thing I can't figure out is autocomplete. I'd like to be able to type in the listbox and have it suggest some options from the list. It works fine in v3, but not v2.
Here are the parts that matter...
$filecheck = "$dir\Apps\Customers.csv"
If(Test-Path -path $filecheck) {
$Customers = Import-CSV "$dir\Apps\Customers.csv" -Header $headers
$List = $customers | Select-Object -Expand name | where {$_ -ne ""} | where {$_ -ne "Name"} | Sort-Object
}
ForEach ($Items in $List) {
$companybox.Items.Add($Items)
}
$companybox.AutoCompleteSource = 'CustomSource'
$companybox.AutoCompleteMode = 'SuggestAppend'
$List | % {$companybox.AutoCompleteCustomSource.AddRange($_) }
$Form1.Controls.Add($companybox)
Any help would be greatly appreciated. I've looked around but haven't found much for v2.
Thanks.
Got it to work. I added -STA to the batch file that runs the script to change it to Single Thread Apartment.
My batch file now looks like this to run the script in STA as administrator...
SET Dir=%~dp0
SET PSPath=%Dir%Script.ps1
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-STA -NoProfile -ExecutionPolicy Bypass -File ""%PSPath%""' -Verb RunAs}";
Another option is to put this in the PS script...
if ([System.Threading.Thread]::CurrentThread.ApartmentState -eq [System.Threading.ApartmentState]::MTA)
{
powershell.exe -Sta -File $MyInvocation.MyCommand.Path
return
}
However, the second option broke the code I have to hide the console window on startup.

Start-Transcript: This host does not support transcription

I want to start a transcript on a Windows Server 2008 R2
Start-Transcript -path C:\Temp\test.txt
"Hello!"
Stop-Transcript
But PowerShell returns the following error message:
Start-Transcript : This host does not support transcription.
How it is possible to activate transcript?
Windows PowerShell v4 ISE and lower do not support transcription. You must use the command line to run the commandlet.
From PowerShell v5 Start-Transcript is supported natively in ISE.
COMPLETE ANSWER (PowerShell ISE 2.0/4.0)::
Having yet another look at this today on another server, I noticed that the latest PowerShell ISE (which also does not allow Start-Transcript) does not have an Output pane, and instead uses the new ConsolePane. Thus the function now is as follows:
Function Start-iseTranscript
{
Param(
[string]$logname = (Get-logNameFromDate -path "C:\fso" -postfix " $(hostname)" -Create)
)
$transcriptHeader = #"
**************************************
Windows PowerShell ISE Transcript Start
Start Time: $((get-date).ToString('yyyyMMddhhmmss'))
UserName: $env:username
UserDomain: $env:USERDNSDOMAIN
ComputerName: $env:COMPUTERNAME
Windows version: $((Get-WmiObject win32_operatingsystem).version)
**************************************
Transcript started. Output file is $logname
"#
$transcriptHeader >> $logname
$psISE.CurrentPowerShellTab.Output.Text >> $logname
#Keep current Prompt
if ($Global:__promptDef -eq $null)
{
$Global:__promptDef = (gci Function:Prompt).Definition
$promptDef = (gci Function:Prompt).Definition
} else
{
$promptDef = $Global:__promptDef
}
$newPromptDef = #'
if ($Host.Version.Major -eq 2)
{
if ($Global:_LastText -ne $psISE.CurrentPowerShellTab.Output.Text)
{
Compare-Object -ReferenceObject ($Global:_LastText.Split("`n")) -DifferenceObject ($psISE.CurrentPowerShellTab.Output.Text.Split("`n"))|?{$_.SideIndicator -eq "=>"}|%{
$_.InputObject.TrimEnd()}|Out-File -FilePath ($Global:_DSTranscript) -Append
$Global:_LastText = $psISE.CurrentPowerShellTab.Output.Text
}
} elseif ($Host.Version.Major -eq 4)
{
if ($Global:_LastText -ne $psISE.CurrentPowerShellTab.ConsolePane.Text)
{
Compare-Object -ReferenceObject ($Global:_LastText.Split("`n")) -DifferenceObject ($psISE.CurrentPowerShellTab.ConsolePane.Text.Split("`n"))|?{$_.SideIndicator -eq "=>"}|%{
$_.InputObject.TrimEnd()}|Out-File -FilePath ($Global:_DSTranscript) -Append
$Global:_LastText = $psISE.CurrentPowerShellTab.ConsolePane.Text
}
}
'# + $promptDef
$Global:_LastText = $psISE.CurrentPowerShellTab.Output.Text
New-Item -Path Function: -Name "Global:Prompt" -Value ([ScriptBlock]::Create($newPromptDef)) -Force|Out-Null
}
Taking over the prompt is incredibly useful for this, however keeping two copies of the Output buffer is not ideal. I've also added in TrimEnd() as PSISE 2.0 likes to append spaces to fill the entire horizontal line width. Not sure if PSISE 4.0 does this too, but it's no problem now anyway.
NEW ANSWER (PowerShell ISE 2.0)::
I have just recently returned to this problem, and there is a way of forcing every update in PowerShell ISE to log out as a command is executed. This relies on the log path being saved in a Global Variable called _DSTranscript. This variable is passed to the Start-iseTranscript function. I have then Hijacked the Prompt function to execute a compare between _LastText and the hostUI Output Text, and append the differences out to the log. It now works a treat.
Function Start-iseTranscript
{
Param(
[string]$logname = (Get-logNameFromDate -path "C:\fso" -postfix " $(hostname)" -Create)
)
$transcriptHeader = #"
**************************************
Windows PowerShell ISE Transcript Start
Start Time: $(get-date)
UserName: $env:username
UserDomain: $env:USERDNSDOMAIN
ComputerName: $env:COMPUTERNAME
Windows version: $((Get-WmiObject win32_operatingsystem).version)
**************************************
Transcript started. Output file is $logname
"#
$transcriptHeader >> $logname
$psISE.CurrentPowerShellTab.Output.Text >> $logname
#Keep current Prompt
if ($__promptDef -eq $null)
{
$__promptDef = (gci Function:Prompt).Definition
$promptDef = (gci Function:Prompt).Definition
} else
{
$promptDef = $__promptDef
}
$newPromptDef = #'
if ($global:_LastText -ne $psISE.CurrentPowerShellTab.Output.Text)
{
Compare-Object -ReferenceObject $global:_LastText.Split("`n") -DifferenceObject $psISE.CurrentPowerShellTab.Output.Text.Split("`n")|?{$_.SideIndicator -eq "=>"}|%{ $_.InputObject.TrimEnd()}|Out-File -FilePath ($Global:_DSTranscript) -Append
$global:_LastText = $psISE.CurrentPowerShellTab.Output.Text
}
'# + $promptDef
New-Item -Path Function: -Name "Global:Prompt" -Value ([ScriptBlock]::Create($newPromptDef)) -Force|Out-Null
}
ORIGINAL ANSWER::
PowerShell ISE does not natively support Transcription. There is a Scripting Guy blog about how to achieve this. Unfortunately this needs to be the last thing that is run in the script. This means that you need to remember to run it before closing the window. I wish this worked better, or there was a way to force it to run on window closure.
This is the function that produces close to the same result as the Start-Transcript feature:
Function Start-iseTranscript
{
Param(
[string]$logname = (Get-logNameFromDate -path "C:\fso" -name "log" -Create)
)
$transcriptHeader = #"
**************************************
Windows PowerShell ISE Transcript Start
Start Time: $(get-date)
UserName: $env:username
UserDomain: $env:USERDNSDOMAIN
ComputerName: $env:COMPUTERNAME
Windows version: $((Get-WmiObject win32_operatingsystem).version)
**************************************
Transcript started. Output file is $logname
"#
$transcriptHeader >> $logname
$psISE.CurrentPowerShellTab.Output.Text >> $logname
} #end function start-iseTranscript
Either accept you can't, or use a host that does support transcripts (like the console host: PowerShell.exe).
The powershell.exe will also generate this error if there is a problem writing to the log file. For example, if the log file was created by an administrator and the user doesn't have permissions to overwrite the log.
Start-Transcript : The host is not currently transcribing.
At D:\Test1.ps1:9 char:1
+ Start-Transcript -Path "$Source\logs\Test.txt"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Start-Transcript], PSInvalidOperationException
+ FullyQualifiedErrorId : InvalidOperation,Microsoft.PowerShell.Commands.StartTranscriptCommand
A good solution is to try using -Append or to make the log file unique by generating a date/time stamp.
Start-Transcript -Path "$Source\logs\Test.txt" -Append
This way a proper error message is generated.
Access to the path 'D:\Test\logs\Test.txt' is denied.
-Force has the same effect as -Append and will generate a permissions error.
Following the tip from #richard here I created a snippet that allows usage of Transaction logs where I need them (scheduled tasks), so I ended having in Windows 2008R2 the following code that can be run from powershell ISE or as a standalone script.
When run in ISE, the log information will be printed on screen
When run as a script, the log information will be saved to a file
if ($Host.Name -eq "Windows PowerShell ISE Host") {
$ISE=$true
} else {
$ISE=$false
}
if (-Not $ISE) {
$Date = Get-Date -f HHmmss_ddyyyy
Start-Transcript -Path "C:\Temp\$Date.log"
}
//////////
code here ...
//////////
if (-Not $ISE) {
Stop-Transcript
}
Tagging on to the amazing answer and work by #dwarfsoft:
if ($Host.Name -match 'ISE' -and $Host.version.Major -lt 4)
{
#Start-Transcript will not work here. Use Start-iseTranscript by #dwarfsoft above
Start-iseTranscript
}
else
{
#Start Transcript Will work here
Start-Transcript
}
In addition to the ISE (which I note the original poster LaPhi doesn't even mention), the other thing that can cause this error is if you're trying to use Start-Transcript within an Invoke-Command scriptblock. For instance if you're running the script on your client machine, and then connecting to the Windows Server 2008 R2 box via Invoke-Command so Start-Transcript outputs to the server.
When run in the local session Start-Transcript works as expected, however when using Invoke-Command the script is run within a remote session on that computer, and remote sessions have certain limitations, one of which is not supporting transcription.