Powershell IF and not condition not working properly - powershell

I'm struggling to have two conditions met, somehow it seems only one of them works. I'm trying to do :
If a user is connected AND NOT at the lock screen, ask for permission. The commands themselves have been verified and work individually but I must be missing something. Here is what i have:
if (Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem | Select-Object -expandproperty UserName -and -not (get-process -ComputerName $poste -name logonui)) {
"ask for permission"
}
Right now it just doesn't go in this code, it skips to the lower part where something else is happening.
What is wrong with my syntax ?
I can work around it and make it work this old fashioned way from my CMD days:
Clear-Variable -name statut_user
$statut_user -eq 0
if (Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem | Select-Object -expandproperty UserName) {$statut_user +=1}
if (-not (get-process -ComputerName $poste -name logonui)) {$statut_user += 1}
if ($statut_user -eq 2) {
"ask for permission"
}
It works, but not as clean as a proper one liner with the two conditions. Thank you for your help!
ANSWER EDIT: Thanks to vonPryz's answer below i ended up using :
$utilisateur = Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem | Select-Object -expandproperty UserName
$ecran_verr = get-process -ComputerName $poste -name logonui
if( -not ($ecran_verr) -and ($utilisateur)) {
"ask for permission"
}

Aim for clarity, not small codebase size. Instead of making WMI calls in the if statement and piping the results, consider something more readable. Like so,
$cs = gwmi -computername $p Win32_ComputerSystem
$uname = $cs | select-object -expandproperty UserName
$logonui = get-process -ComputerName $p -name logonui
if( -not ($logonui) -and ($uname )) {
# Stuff
}
This approach makes it easy to check that the WMI objects contain sensible values, whatever those may be. Then it should be easier to write a concise conditional statement.

While breaking an expression down into multiple steps is always a good idea for debugging, as demonstrated in vonPryz's helpful answer, sometimes you do want the concision of a single expression without auxiliary variables.
What is wrong with my syntax?
You're missing (...) around the Get-WmiObject ... | Select-Object ... pipeline.
To use a command or pipeline as part of a larger expression, you must always enclose it in (...) A command in PowerShell is a call to an executable - be it a cmdlet, function, alias, or external program.
A simple example:
# !! BROKEN: tokens `-eq 'jdoe'` are interpreted as *arguments for Select-Object*
# !! rather than as operator -eq and RHS 'jdoe'
if (Get-WmiObject Win32_ComputerSystem | Select-Object -Expand UserName -ne 'jdoe') {
'not jdoe'
}
# OK: (...) around the pipeline properly embeds it in the overall expression:
if ((Get-WmiObject Win32_ComputerSystem | Select-Object -Expand UserName) -ne 'jdoe') {
'not jdoe'
}
Here's a fixed version of your original command that fixes that improves other aspects too:
if (
(Get-WmiObject –ComputerName $poste –Class Win32_ComputerSystem).UserName `
-and -not `
(Get-Process -ErrorAction SilentlyContinue -ComputerName $poste -name logonui)
) {
"ask for permission"
}
Given that your Get-WmiObject call only ever outputs 1 object, you can access the .UserName property directly, which is also more efficient than piping to Select-Object -ExpandProperty.
Get-Process outputs a non-terminating error if a process by the given name cannot be found, so -ErrorAction SilentlyContinue suppresses that.
Note the use of ` as a line-continuation character, which allows spreading the conditional across multiple lines, making it much more readable.
Note that the ` must be at the very end of the line.

Related

Get Uptime on Each Computer in an Array, Select the machines with the most uptime, and remotely execute a Script on each Machine

The majority of this code was pulled from a blog online, but I think it's exactly the way I need to be tackling this. I want to get the top 4 machines from an OU based on uptime, and run a script that lives on each of the top 4 machines. I know that the problem involves the Array losing access to the Get-ADComputer properties, but I'm unsure of how to pass these new properties back to their original objects. This works as expected until it gets to the foreach loop at the end.
$scriptBlock={
$wmi = Get-WmiObject -Class Win32_OperatingSystem
($wmi.ConvertToDateTime($wmi.LocalDateTime) – $wmi.ConvertToDateTime($wmi.LastBootUpTime)).TotalHours
}
$UpTime = #()
Get-ADComputer -Filter 'ObjectClass -eq "Computer"' -SearchBase "OU=***,OU=***,OU=***,DC=***,DC=***" -SearchScope Subtree `
| ForEach-Object { $Uptime += `
(New-Object psobject -Property #{
"ComputerName" = $_.DNSHostName
"UpTimeHours" = (Invoke-Command -ComputerName $_.DNSHostName -ScriptBlock $scriptBlock)
}
)
}
$UpTime | Where-Object {$_.UpTimeHours -ne ""} | sort-object -property #{Expression="UpTimeHours";Descending=$true} | `
Select-Object -Property ComputerName,#{Name="UpTimeHours"; Expression = {$_.UpTimeHours.ToString("#.##")}} | Select-Object -First 4 |`
Format-Table -AutoSize -OutVariable $Top4.ToString()
foreach ($Server in $Top4.ComputerName) {
Invoke-Command -ComputerName $Server -ScriptBlock {HOSTNAME.EXE}
}
I'm not married to Invoke-Command in the last foreach but am having the same issues when I try to use psexec. Also, I'm running hostname.exe as a check to make sure I'm looping through the correct machines before I point it at my script.
Here's a streamlined version of your code, which heeds the comments on the question:
# Get all computers of interest.
$computers = Get-ADComputer -Filter 'ObjectClass -eq "Computer"' -SearchBase "OU=***,OU=***,OU=***,DC=***,DC=***" -SearchScope Subtree
# Get the computers' up-times in hours.
# The result will be [double] instances, but they're also decorated
# with .PSComputerName properties to identify the computer of origin.
$upTimes = Invoke-Command -ComputerName $computers.ConputerName {
((Get-Date) - (Get-CimInstance -Class Win32_OperatingSystem).LastBootUpTime).TotalHours
}
# Get the top 4 computers by up-time.
$top4 = $upTimes | Sort-Object -Descending | Select-Object -First 4
# Invoke a command on all these 4 computers in parallel.
Invoke-Command -ComputerName $top4.PSComputerName -ScriptBlock { HOSTNAME.EXE }

Get-WmiObject not handling variable as it's typed

Im trying to get list of all computers and logged on user for each computer.
I get the list of computers, and when I query for each string returned, I can get the logged in user.
However, when I use the computer name variable to get all users - I get an error saying that my variable is unknown. I tried casting the computer name to string but that didn't help.
By the way it's the same error that comes up when I type in a wrong computer name, so it has something to do with the type of variable #item, When I print the variable out it's correct, but cannot be used inside the loop.
$obj = Get-ADComputer -Filter 'Name -like "L*"' -Properties * | Select -ExpandProperty Name
foreach ( $item in $obj ) {
$itemString = $item.ToString()
$user = Get-WmiObject –ComputerName $itemString –Class Win32_ComputerSystem | Select-Object UserName | select -expandproperty UserName -first 1
$user = $user.SubString(8)
write-output "Computer: $itemString Username: $user"
}
Get-WmiObject : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At N:\Foreach.ps1:4 char:13
+ $user = Get-WmiObject –ComputerName $itemString –Class Win32_Comp ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject], COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
Your symptom suggests that the offending target computer is simply not available, e.g., due to being disconnected from the network or being powered off.
Your code doesn't deal with this scenario, but is otherwise correct in terms of data types (though given that the .Name property is [string]-typed and you've used Select-Object -ExpandProperty, $obj is an array of strings already, and therefore $item is a single string in the foreach loop, there is no need for a separate $itemString variable obtained with .ToString()).
Note: I'm assuming that the .Name property value is sufficient to identify a computer in the Get-WmiObject call; if it isn't, use Select-Object -ExpandProperty DNSHostName.
Generally, note that Get-WmiObject's -ComputerName parameter is [string[]]-typed, so you can directly pass an array of hostnames to it.
However, regrettably, the [System.Management.Automation.ErrorRecord] instances created by Get-WmiObject when a target computer is unavailable don't contain the offending computer name, so using a single command with later inspection of error output isn't an option (though see the Get-CimInstance alternative at the bottom).
With that in mind, here's a PSv3+ loop-based solution that handles unavailable computers gracefully:
foreach ($computer in (Get-ADComputer -Filter 'Name -like "L*"' -Properties *).Name) {
$computerInfo = Get-WmiObject -ErrorAction SilentlyContinue -ComputerName $computer -Class Win32_ComputerSystem
if (-not $?) { # Call failed, analyze the reason.
if ($Error[0].Exception.HResult -eq 0x800706BA) { # unavailable
# Merely issue a *warning*.
# Alternatively, collect the unavailable names in an array for later use.
Write-Warning "Computer is unavailable: $computer"
} else { # unexpected error, pass it through
Write-Error $Error[0]
}
} else { # success
"Computer: $($computerInfo.Name) Username: $(($computerInfo.UserName -split '\\')[-1])"
}
}
By contrast, Get-CimInstance - recommended instead of Get-WmiObject since its introduction in PSv3 - does add origin information to error records, so a single invocation of Get-CimInstance is sufficient - do note that the results are not guaranteed to arrive in the same order in which the computers were specified, but you do benefit from parallel execution.
# Use a single Get-CimInstance call to target all computers and
# quietly collect errors for later analysis.
$computerNames = (Get-ADComputer -Filter 'Name -like "L*"' -Properties *).Name
Get-CimInstance -ErrorAction SilentlyContinue -ErrorVariable errs `
-ComputerName $computerNames -Class Win32_ComputerSystem | #`
ForEach-Object {
"Computer: $($_.Name) Username: $(($_.UserName -split '\\')[-1])"
}
# Analyze the errors that occurred, if any.
$errs | ForEach-Object {
if ($_.Exception -is [Microsoft.Management.Infrastructure.CimException] -and $_.CategoryInfo.Category -eq 'ConnectionError') {
Write-Warning "Computer is unavailable: $($_.OriginInfo.PSComputerName)"
} else { # unexpected error, pass it through
Write-Error $_
}
}

PowerShell Get-Service is very slow to return results

I have tried several methods for gathering service data and I can't seem to get one to meet all my needs. Get-Service works fine but is very slow when I pipe in a couple Where-Object properties. Get-CimInstance is much faster but I can't figure out how to exclude services. Any ideas?
Here's my code attempts so far. This one is fast until I add the Where-Object. Then it takes 3 times longer if I do:
Get-Service -DisplayName * -ComputerName $Name -Exclude $ExcludedServices | Where-Object { $_.status -eq 'Running' -or $_.StartType -eq 'Automatic' }
This one works much faster but I don't know how to exclude a list of Services if needed:
Get-CimInstance -ClassName Win32_Service -ComputerName $Name | Where-Object { $_.state -eq 'Running' -or $_.StartMode -eq 'Auto' }
I don't know how to exclude a list of Services if needed
Get-CimInstance allows you to impose a WQL WHERE clause constraint on the query:
Get-CimInstance Win32_Service -Filter 'Name != "excludedSvc"'
You can also restrict the items based on the State or StartMode properties inside the query, so the remote computer doesn't have to send back all of the services:
Get-CimInstance Win32_Service -Filter 'Name != "excludedSvc" AND State = "Running" AND StartMode = "Auto"'

powershell remove software from PC

(Get-WmiObject -Class Win32_Product -ComputerName $PCNumber -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "$softwareName" }).Uninstall() | Out-Null
I have following code which works perfectly. The only problem is that I wont to know if the software has been removed or not.This doesn't tells me but the code below does.
This way works for me.
$software = Get-WmiObject -Class Win32_Product -ComputerName $PCNumber -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "$softwareName" }
$soft = $software.Uninstall();
$n = $software.ReturnValue;
if ( $n -eq 0 ){
SOFTWARE HAS BEEN REMOVED.
}
my question is that how do i tell if the software has been removed or not.
using this code.
(Get-WmiObject -Class Win32_Product -ComputerName $PCNumber -ErrorAction SilentlyContinue | Where-Object { $_.Name -match "$softwareName" }).Uninstall() | Out-Null
You have to check the ReturnValue property. When you pipe to Out-Null you are suppressing the output of the operation and there's no way to tell what happened, unless you issue a second call to find if it returns the software in question.
I recommend using the Filter parameter (instead of using Where-Object) to query the software on the server. To be safe you should also pipe the results to the Foreach-Object cmdlet, you never know how many software objects you get back due to the match operation (and you call the Uninstall method as if the result is one object only):
Get-WmiObject -Class Win32_Product -ComputerName $PCNumber -Filter "Name LIKE '%$softwareName%'" | Foreach-Object {
Write-Host "Uninstalling: $($_.Name)"
$rv = $_.Uninstall().ReturnValue
if($rv -eq 0)
{
"$($_.Name) uninstalled successfully"
} # Changed this round bracket to a squigly one to prperly close the scriptblock for "if"
else
{
"There was an error ($rv) uninstalling $($_.Name)"
}
}

Powershell Script to output Installed Programs

I'd like to do this using the win32_product wmi class.
I need the script to simply count the number of products installed and also output the time taken to execute the script.
What I have atm doesn't seem to work correctly:
$count = 0
$products = get-wmiobject -class "Win32_Product"
foreach ($product in $products) {
if ($product.InstallState -eq 5) {
count++
}
}
write-host count
Beware! Using WMI's Win32_Product class, which the question and the previous 2 answers do, is not advised for this purpose.
In a nutshell: using Win32_Product is not an innocuous query because it has side effects. Quoting Microsoft, "[It]... initiates a consistency check of packages installed, verifying and repairing the install." (emphasis mine)
References:
Microsoft Support Reference
Use PowerShell to Quickly Find Installed Software (Hey, Scripting Guy! blog)
Win32_Product WMI Class Replacement
Why Win32_Product is Bad News!
So what is a better (safer) solution?
Marc Carter, writing the guest column in the Hey, Scripting Guy! blog above takes the first volley, providing a custom PowerShell function, back on my system it returned only half as many entries as the Win32_Product invocation. Also, it is a lot of code (3 dozen lines or so). In the comments to his post, however, knutkj offers this much shorter version that does the same thing:
Get-ChildItem -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall |
Get-ItemProperty |
Sort-Object -Property DisplayName |
Select-Object -Property DisplayName, DisplayVersion, InstallLocation
But it does, as I said, the same thing: not provide a full list. But it is a start.
Later in the comments Nick W reported that there are actually 3 registry paths of interest, though not all may be present on every system. Further, when looking at those 3 paths, one has to do some additional filtering.
Combining both of those, adding in a few more output fields, and making the code safe to run in strict mode, I arrived at this simple solution:
function Get-InstalledPrograms()
{
$regLocations = (
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\",
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\"
)
Get-ChildItem ($regLocations | Where { Test-Path $_ } ) |
Get-ItemProperty |
Where {
( (Get-Member -InputObject $_ -Name DisplayName) -and $_.DisplayName -ne $Null) -and
(!(Get-Member -InputObject $_ -Name SystemComponent) -or $_.SystemComponent -ne "1") -and
(!(Get-Member -InputObject $_ -Name ParentKeyName) -or $_.ParentKeyName -eq $Null)
} |
Sort DisplayName |
Select DisplayName, DisplayVersion, Publisher, InstallLocation, InstallDate, URLInfoAbout
}
A bit late on this but the "more powershellish way":
$(Get-WmiObject -Class "Win32_Product" -Filter "InstallState=5").Count
Roman Kuzmin is right about the typo. Correction will solve almost everything.
To make it more powershellish, I would use
get-wmiobject -class "Win32_Product" |
? { $_.InstallState -eq 5 } |
measure-object |
select -exp Count
And considering the time, you can wrap it into measure-command
measure-command {
$count = get-wmiobject -class "Win32_Product" |
? { $_.InstallState -eq 5 } |
measure-object |
select -exp Count
write-host Count: $count
}