I have been working on the following example from one of Don Jones' powershell books as part of my personal development and am having some serious trouble getting the try/catch construct to work as it should. As you can see, when the catch block executes, it sets a variable called $everything_ok to $false - which should trigger the else block in the following code. Which it does - the logfile is appended as per my expectations.
However it does not stop the script from ALSO executing the code in the if block and spewing out 'The RPC Server is unavailable' errors when it tries to query the made-up machine 'NOTONLINE' (Exception type is System.Runtime.InteropServices.COMException).
What makes this even stranger is that I went through the script with breakpoints, checking the contents of the $everything_ok variable along the way, and it never contained the wrong value at any point. So why on earth is the if block still executing for 'NOTONLINE' when the condition I have specified ( if ($everything_ok = $true) ) has not been met?
Am I doing something wrong here?
function get-systeminfo {
<#
.SYNOPSIS
Retrieves Key Information on 1-10 Computers
#>
[cmdletbinding()]
param (
[parameter(mandatory=$true,valuefrompipeline=$true,valuefrompipelinebypropertyname=$true,helpmessage="computer name or ip address")]
[validatecount(1,10)]
[validatenotnullorempty()]
[alias('hostname')]
[string[]]$computername,
[string]$errorlog = "C:\retry.txt",
[switch]$logerrors
)
BEGIN {
write-verbose "Error log will be $errorlog"
}
PROCESS {
foreach ($computer in $computername) {
try {$everything_ok = $true
gwmi win32_operatingsystem -computername $computer -ea stop
} catch {
$everything_ok = $false
write-verbose "$computer not Contactable"
}
if ($everything_ok = $true) {
write-verbose "Querying $computer"
$os = gwmi win32_operatingsystem -computername $computer
$cs = gwmi win32_computersystem -computername $computer
$bios = gwmi win32_bios -computername $computer
$props = #{'ComputerName' = $cs.__SERVER;
'OSVersion' = $os.version;
'SPVersion' = $os.servicepackmajorversion;
'BiosSerial' = $bios.serialnumber;
'Manufacturer' = $cs.manufacturer;
'Model' = $cs.model}
write-verbose "WMI Queries Complete"
$obj = new-object -type psobject -property $props
write-output $obj
}
elseif ($everything_ok = $false) {
if ($logerrors) {
"$computer $_" | out-file $errorlog -append
}
}
}
}
END {}
}
get-systeminfo -host localhost, NOTONLINE -verbose -logerrors
The equals sign in Powershell is used as the assignment operation. -eq is used to test for equality. So your if statement is assigning $true to $everything_ok, which then tests true.
Related
The ForEach loop on this powershell script is failing to run more than one item before dropping out?
Can someone help me on this one?
function Get-RemoteLogonStatus {
[CmdletBinding()]
param(
[string]$ComputerName = ' '
)
ForEach ($line in Get-Content C:\ADComputers.csv)
{
$Computername = $line
if ( Test-Connection -ComputerName $ComputerName -Count 3 -Quiet ) {
try {
Get-WmiObject –ComputerName $ComputerName –Class Win32_ComputerSystem | Select-Object UserName = $lname -ErrorAction Stop | Out-Null
}
catch {
Write-Output 'No user logged in - RESTARTING.'
Shutdown /r /t 0 /M \\$ComputerName
$ComputerName
return
}
Write-Output 'Computer in use.'
$ComputerName
}
else {
Write-Output 'Computer in Use or is Offline.'
$ComputerName
}
}
$error.clear
}
Get-RemoteLogonStatus
Should run more than one item from the file. The file has 4 items for test:
a function is supposed to contain a block of code that can be repeated a number of times. Your function does all in one go, hence I don't see the need for it. Also it has the possibility to take one argument, but you don't pass it.
'return' is not necessary in PowerShell, it will throw the content of a variable without the need for a 'return'.
Select-Object needs a name that is being passed from the pipe, and not an assignment.
inside the try statement you might want to get an output, but if you pipe the line to Out-null you get nothing. and the catch will never grab any error.
the write-output are not clearly positioned, and difficult to understand.
I can infer what you are trying to achieve is: reboot computers in the csv file IF no user is logged in, is that so? In that case it's much simpler:
foreach ($ComputerName in (Get-Content C:\ADComputers.csv)) {
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName | Select-Object UserName
if ($User.UserName -eq $null) {
Restart-Computer -ComputerName $ComputerName -Force
}
}
of if you want to stick to a function and see its purpose see this:
function Restart-Node {
param(
[Parameter(Mandatory=$true)][string]$ComputerName
)
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName | Select-Object UserName
if ($User.UserName -eq $null) {
Restart-Computer -ComputerName $ComputerName -Force
}
}
foreach ($ComputerName in (Get-Content C:\ADComputers.csv)) {
Restart-Node -ComputerName $ComputerName
}
I am trying to write this script to restart computers only if they are Offline. The script for getting user infomration works but I cannot get the variable values for the restart portion at the bottom of the script. Does anyone have a suggestion? I am somewhat new to Powershell, but writing code. Example of my script follows:
Function Get-LoggedOnUser
{
Param
(
$ComputerName = $env:COMPUTERNAME,
$Credential
)
Function Test-RemoteRegistry
{
Param
(
[Parameter(Mandatory = $False)]
[switch]$Enable
,
[Parameter(Mandatory = $False)]
[switch]$Disable
,
[Parameter(ValueFromPipeline=$True)]
[String[]]$ComputerName = $env:COMPUTERNAME
)
Begin
{
$PipelineInput = (-not $PSBOUNDPARAMETERS.ContainsKey("ComputerName")) -and (-not $ComputerName)
Function Test ($Computer)
{
Try
{
[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $Computer) | Out-Null
#20ms faster than Get-Service per computer! Not sure how to handle/check things like the firewall though...
#If we hit here without error Remote Reg is enabled.
If ($Disable)
{
Try
{
Get-Service -Name RemoteRegistry -ComputerName $Computer | Set-Service -Status Stopped -ErrorAction Stop
Return $False
#If we hit here without error Remote Reg is now disabled.
}
Catch
{
Return $True
#If we hit here, we couldn't stop remote registry.
}
}
Else
{
Return $True
}
}
Catch
{
If ($Enable)
{
Try
{
Get-Service -Name RemoteRegistry -ComputerName $Computer | Set-Service -Status Running -ErrorAction Stop
Return $True
#If we hit here without error Remote Reg is now enabled.
}
Catch
{
Return $False
#If we hit here, we couldn't start remote registry.
}
}
Else
{
Return $False
#If we hit here remote registry is disabled.
}
}
}
}
Process
{
If ($PipelineInput)
{
Test $_
}
Else
{
$ComputerName | ForEach-Object {
Test $_
}
}
}
}
Foreach ($Computer in $Computername)
{
$Online = $False
$User = $False
$Locked = $False
If (Test-Connection $Computer -Count 2 -Quiet)
{
$Online = $True
If ($Credential)
{
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer -Credential $Credential | Select-Object -ExpandProperty UserName -ErrorAction Stop
}
Else
{
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $Computer | Select-Object -ExpandProperty UserName -ErrorAction Stop
}
If (Test-RemoteRegistry -Enable -ComputerName $Computer)
{
If ((Get-Process logonui -ComputerName $Computer -ErrorAction SilentlyContinue) -and ($user))
{
$Locked = $True
}
}
}
$Output = New-Object PSObject
$Output | Add-Member noteproperty ComputerName $Computer
$Output | Add-Member noteproperty Online $Online
$Output | Add-Member noteproperty Username $User
$Output | Add-Member noteproperty Locked $Locked
$Output
}
}
Get-LoggedOnUser
If (($Online) -eq $False)
{Shutdown /r t 0 /m \\$Computername}
ELSE
{Write-host 'HELLO $Online $Computername'}
I just want this for a single user as I am using PDQ Inventory to roll out the script. The variables at the end of the script are $null?
Variables defined in a child scope - in which functions run by default - are never seen by the calling scope. See the conceptual about_Scopes help topic
It's best for functions to communicate values to the caller via their output ("return value"), which you're function is already doing: it outputs objects whose properties contain the values of interest.
Therefore:
Get-LoggedOnUser |
ForEach-Object { # Loop over all output objects
# Refer to the object at hand via the automatic $_ variable.
# Note the use of "..." (expandable strings) so as to support
# expansion (string interpolation).
if (-not $_.Online) { Shutdown /r t 0 /m "\\$($_.ComputerName)" }
else { "HELLO $($_.Online) $($_.ComputerName)" }
}
2 functions one calls the other.
The CATCH blocks in the function Export-LoggedOnUser are not being triggered when I leave either the $path parameter or the $ComputerName parameter NULL. The Export-LoggedOnUser function is calling the first function "Get-LoggedOnUser". It too will not trigger the catch block if I leave the $ComputerName Parameter null. I have written these in various ways and they both work as desired except the TRY/CATCH structures do not perform in either function.
The Typical error is some always some variation of a 'ParameterBindingValidationException' which is to be expected except that it is not being handled in the CATCH. I'm flummoxed. Gotta be something simple.
function Get-LoggedOnUser{
[CmdletBinding()]
[Alias()]
Param
(
[Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[ValidateScript({Test-Connection -ComputerName $_ -Quiet -Count 1})]
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName
)
Try{
ForEach($computer in $ComputerName){
$output = #{
'ComputerName' = $computer; }#OutputHashTable
$output.UserName = (Get-WmiObject -Class Win32_ComputerSystem -ComputerName $computer -ErrorAction Stop).Username
[PSCustomObject]$output
}
}
Catch{
Write-host 'You must enter a valid computername'
}
}
#New Function
function Export-LoggedOnUser{
[CmdletBinding()]
[Alias()]
Param(
[Parameter(Mandatory=$True)]
[string]$Path,
[Parameter(Mandatory=$True)]
[string[]]$ComputerName
)
try{
$loggedonuser = Get-LoggedOnUser -ComputerName $ComputerName -ErrorAction stop
}
catch{
Write-Host "You need to provide a Computername"
}
Try{
$loggedonuser | Export-Csv -Path $Path -NoTypeInformation -ErrorAction Stop
}
Catch{
Write-Host 'You must enter a valid path'
}
}
Christian,
If you want to test the $Computername parameter and provide an error message I'd drop the parameter validation and do the following:
Function Test {
Param (
[Parameter(Mandatory=$False)]
[String[]] $ComputerName
)
If ($Null -ne $ComputerName) {
ForEach ($Computer in $ComputerName) {
$GCIMArgs = #{Class = 'Win32_ComputerSystem'
ComputerName = "$computer"
ErrorAction = 'Stop'}
Try { $UserName = (Get-CIMInstance #GCIMArgs ).Username }
Catch { "Error: $Computer is an invalid computer name!" }
<#+-----------------------------------------------------------+
| Place your code her to place $username in your PSObject! |
+-----------------------------------------------------------+
#>
} #End ForEach
} #End If ($Null -ne $ComputerName)
Else { "You must supply a valid array of computer names..." }
} #End Function Test
#-------------------- Main Program ---------------------
Test #("DellXPS8920","Dellxps8700","JanetsLaptop")
If run as shown above you get this output:
Error: JanetsLaptop is an invalid computer name!
Which is correct for my lan since that laptop was not turned on.
If you just call TEST with out the array you get this:
You must supply a valid array of computer names...
FYI: I'm very new to PS and I'm using this as a learning opportunity. Again, I'm trying to find a
specific application on a list of multiple remote devices and determine the version number of the
application on their corresponding host system. I attempted this via a registry query (found this to
be challenging) and then I used Get-WMIObject. As of now, I'm working with this as my script. It's
not producing any output; instead, it returns to the command prompt with no errors or messages.
Script to find specific application and version in multiple remote devices:
$Servers = Get-Content -Path C:\\files\Serverlist.txt
$CIMSession = New-CIMSession -ComputerName $Servers Get-Credentials
$Vendor = "App Name"
foreach($Serv in $Servers) {
If(Test-Connection -ComputerName $Serv -Count 1 -Quiet) {
$Status = Get-Ciminstance Win32_Product -Computername $Serv | Where-object {$_.Version -contains
$Vendor}
if($Status) {
Out-file -Filepath C:\\files\AppVerResults.txt
}
}
}
I also tried adjusting the following section of the script as shown below but it presented me with the error "Get-CimInstance : Access is denied." Is this error message due to group policy or so? I am able to remote into the device corresponding to the message via RDP.
if($Status) {
$Servers + " - "
$Status | Out-file -Filepath C:\\files\AppVerResults.txt
}
}
}
Should I go about it via invoke-command or registry query? I'm slowly picking things up so I'll continue my research but I was hoping to get some advice in the meantime.
I still believe searching the registry is the easier way to go unless you have the specific file path for the .exe.
Use this function to find software on a remote, or local PC. Theres a filter option by specifying -SoftwareName (to look for).
Find-Software -ComputerName Remote_ComputerName -SoftwareName 'SQL'
Also accepts pipeline input, as well as multiple computer names to query for.
Find-Software -ComputerName ComputerOne, ComputerTwo, ComputerThree -SoftwareName 'SQL'
'ComputerOne','ComputerTwo' | Find-Software -SoftwareName 'SQL'
Exporting is also allowed by piping to an Export-* cmdlet.
Heres the code:
Function Find-Software {
[cmdletBinding()]
Param(
[Parameter(Mandatory=$false,
ValueFromPipeLine=$true,
ValueFromPipeLineByPropertyName=$true)]
[Alias('cn','name')]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory=$false)]
[String]$SoftwareName
)
Begin{
#Get Computer Names to check software version for
$Server_List = Get-Content -Path "C:\files\Serverlist.txt"
#Get Credentials for Script Scope once.
$Credentials = Get-Credential
}
Process{
if($PSBoundParameters.ContainsKey('SoftwareName')){
foreach($Computer in $ComputerName){
Try{
$PSSession = New-PSSession -ComputerName $Computer -Credential $Credentials -EnableNetworkAccess -ErrorAction Stop
$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" } -Session $PSSession
$Software_List = $Software_List | Where-Object -FilterScript {$_.DisplayName -match $SoftwareName} | Sort-Object -Property DisplayName
foreach($Software in $Software_List){
if($Software){
[PSCustomObject]#{
"Computer Name" = $Computer
"Software Name" = $Software.DisplayName
" Version " = $Software.DisplayVersion
}
} else {
[PSCustomObject]#{
"Computer Name" = $Computer
"Software Name" = "Not found"
" Version " = $null
}
}
}
} Catch {
"Unable to connect to PC: $Computer"
"Error: $($Error[0].Message.Split('.')[1].Trim())"
}
}
} else {
foreach($Computer in $ComputerName){
Try{
$PSSession = New-PSSession -ComputerName $Computer -Credential $Credentials -EnableNetworkAccess -ErrorAction Stop
$Software_List = Invoke-Command -ScriptBlock {
Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" } -Session $PSSession
$Software_List = $Software_List | Sort-Object -Property DisplayName
foreach($Software in $Software_List){
[PSCustomObject]#{
"Computer Name" = $Computer
"Software Name" = $Software.DisplayName
" Version " = $Software.DisplayVersion
}
}
} Catch {
"Unable to connect to PC: $Computer"
"Error: $($Error[0].Message.Split('.')[1].Trim())"
}
}
} #end ELSE statement
} #end PROCESS block
End {
if(Get-PSSession){
Get-PSSession | Remove-PSSession
}
} #end END block - Perform Session Clean Up
} #end FUNCTION
Simply modify it to fit your needs :)
I'm looking for help with my script again :)
I have a script that will query list of servers to find if specific service is installed. This works fine. However, I know that there are some servers in my list that I don't have access to, or there are different credentials. How do I make this visible in output? Because I only get output that service is not installed, which is not true, I just don't have correct credentials.
$name = "BESClient"
$servers = Get-content C:\list.txt
function Confirm-WindowsServiceExists($name)
{
if (Get-Service -Name $name -Computername $server -ErrorAction Continue)
{
Write-Host "$name Exists on $server"
return $true
}
Write-Host "$name does not exist on $server"
return $false
}
ForEach ($server in $servers) {Confirm-WindowsServiceExists($name)}
Also, I'd like to have output formatted into the one line, e.g.:
Server1 Service running
Server2 Service not installed
Server3 no access
etc...
Thanks a lot for any help.
Here's an option which just displays the content of the error on failure:
function Confirm-WindowsServiceExists($name)
{
if (Get-Service -Name $name -Computername $server -ErrorAction SilentlyContinue -ErrorVariable WindowsServiceExistsError)
{
Write-Host "$name Exists on $server"
return $true
}
if ($WindowsServiceExistsError)
{
Write-Host "$server" $WindowsServiceExistsError[0].exception.message
}
return $false
}
As for the second part of the question #arco444 has described the correct approach.
Here's a WMI solution. Any errors you get from attempting to connect to remote computers will be caught with the try/catch blocks. The result of each operation will be stored to a custom object and added to the array that holds the results of all the operations.
$result = #()
$name = "BESClient"
$servers = Get-Content C:\list.txt
$cred = Get-Credential
foreach($server in $servers) {
Try {
$s = gwmi win32_service -computername $server -credential $cred -ErrorAction Stop | ? { $_.name -eq $name }
$o = New-Object PSObject -Property #{ server=$server; status=$s.state }
$result += ,$o
}
Catch {
$o = New-Object PSObject -Property #{ server=$server; status=$_.Exception.message }
$result += ,$o
}
}
$result | Format-Table -AutoSize
You should end up with something like this:
server state
------ -----
s1 running
s4 stopped
s2 The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)