Uninstalled application still shows in win32_product - powershell

I have an application that seems to have left a mess behind. I've uninstalled the app and it seems to have progressed normally.
No longer shows in Programs & Features. Nothing in the registry locations:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer
Nothing even when searching.
When I try to install a newer version I get an error that the previous version needs to be removed first.
I finally found something when I looked in Win32_Product using PS:
Get-WmiObject Win32_Product | Sort-Object Name | Format-Table IdentifyingNumber, Name, LocalPackage
Question is, how can I get it out of here with a script? I can't run an uninstall, get errors. I have been able to use MSI Cleanup Utility to remove but I'd like to be able to do something more automated. Estimating that there's about 200 machines in this state.

There are some very good reasons to never use Win32_Product. If you Google, there's lots of explanations, but here's one of the first hits, Please Stop Using Win32_Product To Find Installed Software. Alternatives Inside!
Of course, that's not really your question, Win32_Product was just how you located it. It's possible the program installation data is in a different location.
Try looking through:
'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\'
'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\'
'Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\'
There's an awesome script on the Gallery called Get-RemoteProgram.ps1 it packages a function by the same name so dot source it into your session like:
. <Path>\Get-RemoteProgram.ps1
Once you are in you can search for the program and include the registry path in the output. There're plenty of examples in the help file but something like:
Get-RemoteProgram -ComputerName $env:computername -IncludeProgram ^Office -ProgramRegExMatch -DisplayRegPath
Once you know the location I would look for an UninstallString value. If yes, I'd then think about how to get it to run silently, which if it's an MSI package should be pretty straight forward. Once you've got it worked out simply wrap some PowerShell code around it to invoke and monitor it through to completion.
Update from Comments:
Obviously I'd have trouble figuring this out from a afar. I posted above because it would find something in the registry. Partly because you hadn't listed the Wow6432... location.
Given my earlier statements I'm not going to try testing Win32_Product on my own. However, my next step would be to figure out what Win32_Product is finding. In that case I would start with Process Monitor. It will take some work, but may illuminate what Win32_Process is finding.
The other thing I can suggest is to observe a fresh installation of the software on another system. By snapshotting the registry, and perhaps a directory listing before and after you may find additional bread crumbs.
You can also use a secondary instance of the program to harvest the uninstall string, then try running it on the concerned system to see what happens.

Piggy backing off Steven's answer, you can use something along the lines of this:
Function Uninstall-Software {
[CmdletBinding()]
[CmdletBinding()]
param(
[Parameter(Mandatory=$false,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias('cn','name')]
[string[]]$ComputerName,
[Parameter(Mandatory=$false)]
[switch]$Quiet
)
Begin{
$Date_Now = Get-Date
}
Process{
if($Quiet){
foreach($Computer in $ComputerName){
try{
$PSSession = New-PSSession -ComputerName $Computer -ErrorAction Stop
[array]$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"} -Session $PSSession
[array]$Software_List = $Software_List | Sort-Object -Property DisplayName -Unique
$(for($i=0; $i -lt $Software_List.Count; $i++){
[PSCustomObject]#{
'Display Name' ="${i}.) $($Software_List[$i].DisplayName)"
' Version ' = $Software_List[$i].DisplayVersion
}
} ) | Out-Host
$QS = Read-Host -Prompt "Enter Number of software to uninstall"
$QS = $QS -split ','
if([string]::IsNullOrEmpty("$QS") -eq $true){"Null string"; break}
foreach ($Q in $QS) {
if($Software_List[$Q].QuietUninstallString -eq $null){
$Quiet_Switch = '/quiet'
$Uninstall_String = $Software_List[$Q].UninstallString
Invoke-Command -ScriptBlock { & cmd /c $Using:Uninstall_String $Using:Quiet_Switch /norestart } -Session $PSSession -EnableNetworkAccess
}
else{
$Uninstall_String = $Software_List[$Q].QuietUninstallString
Invoke-Command -ScriptBlock { & cmd /c $Using:Uninstall_String /norestart } -Session $PSSession -EnableNetworkAccess
}
#& cmd /c $Uninstall_String $Quiet_Switch /norestart
}
} Catch [System.Management.Automation.RuntimeException] {
$Error[0].Message.Split('.')[1].Trim()
}
}
}
Else{
foreach($Computer in $ComputerName){
try{
$PSSession = New-PSSession -ComputerName $Computer -ErrorAction Stop
[array]$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"} -Session $PSSession
[array]$Software_List = $Software_List | Sort-Object -Property DisplayName -Unique
$(for($i=0; $i -lt $Software_List.Count; $i++){
[PSCustomObject]#{
'Display Name' ="${i}.) $($Software_List[$i].DisplayName)"
' Version ' = $Software_List[$i].DisplayVersion
}
} ) | Out-Host
$QS = Read-Host -Prompt "Enter Number of software to uninstall"
$QS = $QS -split ','
if([string]::IsNullOrEmpty("$QS") -eq $true){"Null string"; break}
foreach($Q in $QS){
$Uninstall_String = $Software_List[$Q].UninstallString
& cmd /c $Uninstall_String /norestart
}
} Catch [System.Management.Automation.RuntimeException] {
$Error[0].Message.Split('.')[1].Trim()
}
}
}
}
}
Calling the function gives you 2 options.
Interactive: Uninstall-Software
Non-Interactive: Uninstall-Software -Quiet
As you can imagine, the interactive choice doesn't work for remote computers, but using -Quiet will.
Uninstall-Software -ComputerName RemoteComputer -Quiet
This is a script I made a while ago and never finished it so it's all over the place. Works for the most part but, can use some serious work. I just dont care for it anymore.
Running the function will make a number selection out of your list of installed software pulled from the registry. So, all you have to do is select the number of software to uninstall, or select multiple.
Hopefully this gets you on the right track following Stevens comments in regards to using the uninstall string to uninstall software, instead of using CIM methods.
Please don't mark this as the answer, thought i'd share an unfinished script that works.

Related

Ping and Install applications for multiple PCs

I hope someone can help me with the following script:
The script I built works for 1 computer. It asks the user to enter the computer name, and It will check if the computer is ONLINE, if it is it will copy and install the application.
Now, I want to add another functionality. I want users to create a TXT file with all the computer names (line by line) and save them somewhere in the computer. The user will run the script, and a Windows Explorer would open requesting the location of the TXT file. So, the script will collect that data (I might be wrong, but should I use arrays for that?) From there, I am assuming a ForEach statement should do it right?
The most difficult (for me) part is... if the computer is OFFLINE, it will skip that computer, and go for the next one... when it reaches the end of the lines, it should go back to the first line and check if the computer (that was offline) is online. The tricky part is... if the computer already has the app installed, it should skip it, and continue to keep pinging computers and so on. I want the script to run until someone manually stops it, installed the app to all the pcs, or runs for 3 days.
This might be too much to ask, but... it would also be nice to see a report of all the computers where the app was successfully installed (I am sure this should be on the ForEACH portion).
Thank you all
function HarvesterMenu {
$PCName = read-host -Prompt "Enter the computer name"
$PCName = $PCName -replace '(^\s+|\s+$)','' -replace '\s+',' '
$pingPC = Test-Connection -ComputerName $PCName -Quiet -Count 1 -ErrorAction SilentlyContinue
if ($pingPC -eq $True) {
Write-host "ONLINE"
harvesterInstall($PCName)
}
else {
Write-host "OFFLINE"
}
}
function HarvesterInstall($InstallOnPC) {
$Sourcefile = "\\SERVER1\discovery03$\HarvesterRemote.msi"
Copy-Item -Path $Sourcefile -Destination "\\$InstallOnPC\c$\windows\temp\HarvesterRemote.msi"
Invoke-Command -ComputerName $InstallOnPC -ScriptBlock {
Start-Process c:\windows\temp\HarvesterRemote.msi -ArgumentList "/quiet" -Wait
}
Start-Sleep 3
$InstCompleted = Test-Path -Path '\\$InstallOnPC\C$\Program Files (x86)\Labs\Harvester Remote\Harvester\Harvester.exe'
Write-Host $InstCompleted
if ($InstCompleted -eq $True) {
Write-host "Harvester was successfuly installed on $InstallOnPC"
}
else {
Write-host "Something went wrong. Ping the server and try again"
}
$BacktoMenu = read-host -Prompt "Press Y to go back to the Main Menu or N to exist"
if ($backtoMenu -eq "y") {
harvesterMenu
}
else {
Exit
}
}
HarvesterMenu
You can do that easily with a Foreach-Loop.
U need a File with the pc's u wanna install the application. maybe .txt or .csv
Then u can just address it in a foreach.
something like that:
$InputFile = 'C:\Temp\lIST.csv'
$addresses = get-content $InputFile
foreach($address in $addresses) {
if (test-Connection -ComputerName $address -Count 1 -Quiet ) {
write-Host "$address Online"
#ur harvesterinstall
}
else
{
Write-Warning "$address Offline"
}
}

Using Invoke-Command with Variables in ScriptBlock

New to PowerShell and learning through writing random scripts using the help info. I've tried the following 3 ways to properly get variables into the ScriptBlock(along with way too many small variations to list) with listed error message wrapped in **:
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $DaysAgo | Out-File $HOME\$location\$filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $Using:$DaysAgo | Out-File $Using:HOME\$Using:location\$Using:filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
Write-Host "Processing..."
$DaysAgo = [datetime]::Now.AddDays(-$days)
$parameters = #{
ScriptBlock = { Param ($Arg1,$Arg2,$Arg3) Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object source -eq DCOM | where TimeGenerated -ge $Arg1 | Out-File "$HOME\$Arg2\$Arg3.txt"}}
JobName = "DCOM"
ArgumentList = ($DaysAgo,$location,$filename)
}
Invoke-Command #parameters
Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for the argument, and then try running the command again.
I'm just looking to have user input how far back they want to view Event Logs, where to save it, and what to name it. I've been able to work my way through everything so far until I hit the Invoke-Command line and haven't been able to get through it. I prefer the one line style of 1 and 2 over the parameters style, however after spending way too much time using the help_Invoke-Command-full and googling I'm throwing in the towel over what I'm sure is a simple error on my syntax.
You can use $args inside the scriptblock, see an example:
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {
Get-EventLog -logname system | Where-Object EntryType -eq Warning |
where TimeGenerated -ge $args[0] |
Out-File $HOME\$location\$filename.txt
} -ArgumentList $DaysAgo
Add the arguments at the end of the Invoke-Command like in the example and use $args[0] for the first argument, $args[1] for the second and so on...
This works for me. The computer is localhost as a test, at an elevated prompt, which you would need for the system log anyway. Level 3 is warning. If it was for the same computer you wouldn't need invoke-command at all.
$location = 'foo'
$filename = 'myfile'
$date = get-date
$daysago = $date.adddays(-1)
invoke-command localhost { param($daysago, $location, $filename)
get-winevent #{logname = 'system'; level = 3; starttime = $daysago} |
out-file $home\$location\$filename.txt } -args $daysago,$location,$filename
In order to use Invoke-Command's -AsJob switch, you must execute code remotely, such as by targeting a different computer with the -ComputerName or -Session arguments.
In the absence of such arguments, your command would run locally, but it fails due to the syntactic restriction described above.
If you want to run a job locally, use Start-Job directly:
$job = Start-Job -Name JobEventLog -ScriptBlock {
Get-EventLog -logname system |
Where-Object EntryType -eq Warning |
Where-Object TimeGenerated -ge $using:DaysAgo |
Out-File $HOME\$using:location\$using:filename.txt
}
Note: Since your background script block references variables from the caller's scope, they must be referenced via the $using: scope (as you've also done in your last Invoke-Command-based attempt). This requirement also applies to script blocks executed remotely, such as via Invoke-Command -ComputerName - see this answer for background information. The alternative is to pass arguments to the script block, via the -ArgumentList (-Args) parameter, though the $using: approach is usually simpler.
Start-Job returns a job-information object (System.Management.Automation.Job), which you can use to monitor the progress of and obtain output from the background job, using the various *-Job cmdlets, notably Wait-Job and Receive-Job - see the about_Jobs conceptual help topic.
Generally, using Invoke-Command for local code execution, while technically supported, is rarely necessary.
For direct, synchronous invocation (not as a job) of a command or script block, use &, the call operator (not needed for single commands, as long as the command name isn't quoted or specified via a variable), or, for execution directly in the caller's scope, ., the dot-sourcing operator (. { ... }).
You have a couple of options. Since you're only running this on your local machine, you can use Start-Job instead of Invoke-Command.
That being said, the problem that you're running into is 2-fold. First, if you're running the Invoke-Command cmdlet, you'll need to specify the ComputerName parameter. Even though it's an optional parameter, you'll need to use it to tell Powershell which parameter set you're using, otherwise it's going to get confused.
Secondly, you'll need to pass the arguments into the scriptblock. This is because Start-Job and Invoke-Command are part of PSRemoting and will actually look for environment variables on the specified computer instead of variables that you've declared in your script.
Here's what worked for me:
Invoke-Command -ComputerName $(hostname) -AsJob -JobName "TestJob" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename
The Invoke-Command option is powerful if you're wanting to get this information from other devices on your network.
And here's the Start-Job version:
Start-Job -Name "TestJob2" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename

Running commands as logged on user (remotely)

Thought I would share this quick function I made for myself, feel free to adapt it and improve it according to your needs.
Sometimes you want to run commands as the logged on user of a remote computer.
As you know, some commands show output for the user who runs it and if you run the same command with Invoke-Command, it won't return the user's information, but yours). Get-Printer is an example amongst many others.
There is no easy, quick way of running commands as the logged on user natively without any third-party apps like PsExec or others so I made this quick function that uses VBS, PS1 and Scheduled Task to make it happen.
It runs completly silently for the user (thanks to the VBS) and the output is shown in your console. Please note it assumes the remote computer has a C:\TEMP.
Created in a Windows 10, powershell v 5.1.17763.503 environement.
I don't pretend it's final and perfect, it's the simplest way I found to do what is needed and I just wanted to share it with you guys as it can be very useful!
Check the comments for explanation of the code and feel free to use it as you wish. Please share your version as I'm curious to see people improve it. A good idea would be to make it support multiple computers, but as I said it's a quick function I did I don't have too much time to put into refining it.
That being said, I had no problems using it multiple times as is :)
*Output returned is in form of a string, if you want to have a proper object, add '| ConvertFrom-String' and play with it :)
PLEASE NOTE: The surefire way of grabbing the username of who is currently logged on is via QWINSTA (since Win32_ComputerSystem - Username is only reliable if a user is logged on LOCALLY, it won't be right if a user is using RDP/RemoteDesktop). So this is what I used to grab the username, however, please note that in our french environement the name of the username property in QWINSTA is "UTILISATEUR",so you have to change that to your needs (english or other language) for it to work. If I remember correctly, it's "USERNAME" in english.
On this line:
$LoggedOnUser = (qwinsta /SERVER:$ComputerName) -replace '\s{2,22}', ',' | ConvertFrom-Csv | Where-Object {$_ -like "*Acti*"} | Select-Object -ExpandProperty UTILISATEUR
See code in the answer below.
function RunAsUser {
Param ($ComputerName,$Scriptblock)
#Check that computer is reachable
Write-host "Checking that $ComputerName is online..."
if (!(Test-Connection $ComputerName -Count 1 -Quiet)) {
Write-Host "$ComputerName is offline" -ForegroundColor Red
break
}
#Check that PsRemoting works (test Invoke-Command and if it doesn't work, do 'Enable-PsRemoting' via WMI method).
#*You might have the adjust this one to suit your environement.
#Where I work, WMI is always working, so when PsRemoting isn't, I enable it via WMI first.
Write-host "Checking that PsRemoting is enabled on $ComputerName"
if (!(invoke-command $ComputerName { "test" } -ErrorAction SilentlyContinue)) {
Invoke-WmiMethod -ComputerName $ComputerName -Path win32_process -Name create -ArgumentList "powershell.exe -command Enable-PSRemoting -SkipNetworkProfileCheck -Force" | Out-Null
do {
Start-Sleep -Milliseconds 200
} until (invoke-command $ComputerName { "test" } -ErrorAction SilentlyContinue)
}
#Check that a user is logged on the computer
Write-host "Checking that a user is logged on to $ComputerName..."
$LoggedOnUser = (qwinsta /SERVER:$ComputerName) -replace '\s{2,22}', ',' | ConvertFrom-Csv | Where-Object {$_ -like "*Acti*"} | Select-Object -ExpandProperty UTILISATEUR
if (!($LoggedOnUser) ) {
Write-Host "No user is logged on to $ComputerName" -ForegroundColor Red
break
}
#Creates a VBS file that will run the scriptblock completly silently (prevents the user from seeing a flashing powershell window)
#"
Dim wshell, PowerShellResult
set wshell = CreateObject("WScript.Shell")
Const WindowStyle = 0
Const WaitOnReturn = True
For Each strArg In WScript.Arguments
arg = arg & " " & strArg
Next 'strArg
PowerShellResult = wshell.run ("PowerShell " & arg & "; exit $LASTEXITCODE", WindowStyle, WaitOnReturn)
WScript.Quit(PowerShellResult)
"# | out-file "\\$ComputerName\C$\TEMP\RAU.vbs" -Encoding ascii -force
#Creates a script file from the specified '-Scriptblock' parameter which will be ran as the logged on user by the scheduled task created below.
#Adds 'Start-Transcript and Stop-Transcript' for logging the output.
$Scriptblock = "Start-Transcript C:\TEMP\RAU.log -force" + $Scriptblock + "Stop-Transcript"
$Scriptblock | out-file "\\$ComputerName\C$\TEMP\RAU.ps1" -Encoding utf8 -force
#On the remote computer, create a scheduled task that runs the .ps1 script silently in the user's context (with the help of the vbs)
Write-host "Running task on $ComputerName..."
Invoke-Command -ComputerName $ComputerName -ArgumentList $LoggedOnUser -ScriptBlock {
param($loggedOnUser)
$SchTaskParameters = #{
TaskName = "RAU"
Description = "-"
Action = (New-ScheduledTaskAction -Execute "wscript.exe" -Argument "C:\temp\RAU.vbs C:\temp\RAU.ps1")
Settings = (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd)
RunLevel = "Highest"
User = $LoggedOnUser
Force = $true
}
#Register and Start the task
Register-ScheduledTask #SchTaskParameters | Out-Null
Start-ScheduledTask -TaskName "RAU"
#Wait until the task finishes before continuing
do {
Write-host "Waiting for task to finish..."
$ScheduledTaskState = Get-ScheduledTask -TaskName "RAU" | Select-Object -ExpandProperty state
start-sleep 1
} until ( $ScheduledTaskState -eq "Ready" )
#Delete the task
Unregister-ScheduledTask -TaskName "RAU" -Confirm:$false
}
Write-host "Task completed on $ComputerName"
#Grab the output of the script from the transcript and remove the header (first 19) and footer (last 5)
$RawOutput = Get-Content "\\$ComputerName\C$\temp\RAU.log" | Select-Object -Skip 19
$FinalOutput = $RawOutput[0..($RawOutput.length-5)]
#Shows output
return $FinalOutput
#Delete the output file and script files
Remove-Item "\\$ComputerName\C$\temp\RAU.log" -force
Remove-Item "\\$ComputerName\C$\temp\RAU.vbs" -force
Remove-Item "\\$ComputerName\C$\temp\RAU.ps1" -force
}
#____________________________________________________
#Example command
#Note: Sometimes Start-Transcript doesn't show the output for a certain command, so if you run into empty output, add: ' | out-host' or '| out-default' at the end of the command not showing output.
$Results = RunAsUser -ComputerName COMP123 -Scriptblock {
get-printer | Select-Object name,drivername,portname | Out-host
}
$Results
#If needed, you can turn the output (which is a string for the moment) to a proper powershell object with ' | ConvertFrom-String'

PowerShell Invoke-Command Returns Blank Data?

Been trying to solve this for a bit and can't seem to figure it out.
I have the following script:
$Servers = Get-Content -Path "C:\Utilities_PowerShell\ServerList.txt"
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus # Checks for status of IIS services
{
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3)
{
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
}
else
{
Write-Host " No IIS service(s) were found..." -foreground "red"
}
}
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock ${function:IISServiceStatus} -AsJob -ArgumentList $IISServiceName1, $IISServiceName2, $IISServiceName3, $IISarrService, $IISarrServiceCheck | Wait-Job | Receive-Job
Write-Host " "
}
Whenever I run it, all I get is the output of:
Status of IIS service(s) on *PC* :
If I run the function outside of a loop/invoke-command, the results are absolutely perfect. What is wrong with my remote loop?
I've tried putting the variables inside the function, I've tried running invoke-command without the argument list, etc.
Update: 3/17/16
Turns out...if I run my actual script as is, the result of $EndJobs is weird in that it outputs ALL services in one table and then the three IIS services in another table. This would explain why when I run my invoke-command (stopIIS) scriptblock...I had to reboot the whole server because it took all of the services down.
These functions run PERFECTLY when not run via remote/invoke-command.
What the heck...invoke-command is seriously screwing with my stuff!
Anyone have any ideas/tips on how I can run my local script (which works 100%) on a set of servers from a text file without weird issues like this? Is invoke-command the only way?
do you have the same problem if you wrap it all into the script block like this?
$Servers = Get-Content 'C:\Utilities_PowerShell\ServerList.txt'
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock {
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus { # Checks for status of IIS services
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3) {
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
} else {
Write-Host ' No IIS service(s) were found...' -ForegroundColor Red
}
}
IISServiceStatus $IISServiceName1 $IISServiceName2 $IISServiceName3 $IISarrService $IISarrServiceCheck
} -AsJob | Wait-Job | Receive-Job
Write-Host ' '
}
$EndJobs
I'm having a similar issue. I'm using credssp to test 2nd hop auth for an automation for shutting down a production environment cleanly. My script has 3 sections; session setup, the invoke, session teardown. If I run each piece separately, I get output. If I run the whole script, I get blank lines matching the amount of output I get when I run them separately... there's nothing fancy in my invoke (backtick line continuation - I prefer Python's formatting paradigm better than Powershell/C#):
Invoke-Command `
-Session $workingSession `
-ScriptBlock {
get-service *spool* -ComputerName server01
}

Check if a Registry Path Exists in Remote Machine

I have used Power Shell to check if a path exists using this command . powershell test-path "HKCU:\Software\Microsoft\Windows" now how can the same be extended to remote machine. What is the syntax if i want to test a registry path in Remote machine, i tried powershell test-path "\\machinename\HKCU:\Software\Microsoft\Windows" and its not working. Suggest some way to test it.
You can access it as outlined here: http://powershell.com/cs/blogs/tips/archive/2011/02/15/accessing-registry-remote.aspx
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', 'server123')
$key = $reg.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall')
$key.GetSubKeyNames() | ForEach-Object {
$subkey = $key.OpenSubKey($_)
$i = #{}
$i.Name = $subkey.GetValue('DisplayName')
$i.Version = $subkey.GetValue('DisplayVersion')
New-Object PSObject -Property $i
$subkey.Close()
}
$key.Close()
$reg.Close()
An alternative is to enable PSRemoting and use invoke-command on the remote machine and effectively run the same command as what you would run on the local box.
You cannot connect to the current user hive of a remote computer. Here's an example of using the Remote Regitry module to check if a remote key exists in the hklm hive of a remote server. The module can be found on codeplex: psremoteregistry.codeplex.con
Test-RegKey -ComputerName server1 -Key software\microsoft\winows -Hive LocalNachine
This site helped me. The code basically checks for one key and then checks for another one if the first one does not exist. It also verifies that the subkey exists before trying to read a value from it. If neither exist a try / catch can help deal with that.
$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $computerName)
$regkey = $reg.OpenSubkey("SOFTWARE\\Symantec\\Symantec Endpoint Protection\\AV\\Storages\\Filesystem\\RealTimeScan")
if(-not $regkey) {
$regkey = $reg.OpenSubkey("SOFTWARE\\Wow6432Node\\Symantec\\Symantec Endpoint Protection\\AV\\Storages\\Filesystem\\RealTimeScan")
}
$autoProtectStatus = ""
$null = ""
if ($regkey.GetValue("OnOff", $null) -ne $null) {
$autoProtectStatus = $regkey.GetValue("OnOff")
}
if ($autoProtectStatus -eq 1) {
$autoProtectStatus = "Protection On"
} elseif ($autoProtectStatus -eq 0) {
$autoProtectStatus = "Protection Off"
} else {
$autoProtectStatus = "Unknown State"
}
So much of the Q&A is 3 years old or older. I suspect the newer versions of Powershell must have cleaned up a lot of this. Having said that, there is still not a direct command to check the registry on a remote computer (to the best of my knowledge). This works - it checks if .NET 4.6.2 is installed (is it simpler than the other answers though?)
invoke-command -computername <NetBiosName> -scriptblock {test-path -Path
"HKLM:\Software\Microsoft\.NetFramework\v4.0.30319\SKUs\.NetFramework,
Version=v4.6.2"}
You can also put the scriptblock content into a *.ps1 file (everything inside the {} and then invoke it with: Invoke-Command -ComputerName NetBiosName -FilePath "FQLP"