I am trying to get monitor information from workstations. When only one monitor my code works all right. But on multiple monitors it fails.
$connections = get-ciminstance -namespace root/wmi -classname
WmiMonitorConnectionParams
$videooutput = "$($($connections.VideoOutputTechnology))"
foreach ($output in $videooutput){
if ($output -eq 10) {write-host "DP"}
if ($output -eq 4) {write-host "DVI"}
if ($output -eq 5) {write-host "HDMI"}
if ($output -eq 0) {write-host "VGA"}
else {write-host "unknown"}
}
The issue is the output of the $ videooutput. It returns multiple monitors like this:
4 10
instead of this:
4
10
So in a multiple monitor situation I get "unknown"
Replace "$($($connections.VideoOutputTechnology))" with $connections.VideoOutputTechnology.
"$($($connections.VideoOutputTechnology))", aside from needlessly using $(...) twice, stringifies the array returned by $connections.VideoOutputTechnology, which by default creates a single string containing the (stringified) elements as a space-separated list.
As an aside, just to clarify: Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, redirect it to a file. (In PSv5+ Write-Host writes to the information stream, whose output can be captured, but only via 6>; see this answer.)
Also, using a switch statement instead of a foreach loop with multiple if statements enables a simpler solution.
To put it all together:
$connections = get-ciminstance -namespace root/wmi -classname WmiMonitorConnectionParams
$videooutput = $connections.VideoOutputTechnology
switch ($videooutput) {
10 { "DP"; continue }
4 { "DVI"; continue }
5 { "HDMI"; continue}
0 { "VGA"; continue }
default { "unknown"}
}
Related
function Show-Menu { #Create the Show-Menu function
param ([string]$Title = 'Functions') #Sets title
Clear-Host
Write-Host "`t6: Reboot History." -foregroundcolor white
Write-Host "`tQ: Enter 'Q' to quit."
} #close of create show menu function
#Begin Main Menu
do
{
Show-Menu #Displays created menu above
$Selection = $(Write-Host "`tMake your selection: " -foregroundcolor Red -nonewline; Read-Host)
switch ($selection) #Begin switch selection
{
#===Reboot History===
'6' {
$Workstation = $(Write-Host "Workstation\IP Address" -nonewline -foregroundcolor DarkGreen) + $(Write-Host "(Use IP for remote users)?: " -NoNewline; Read-Host)
$DaysFromToday = Read-Host "How many days would you like to go back?"
$MaxEvents = Read-Host "How many events would you like to view?"
$EventList = Get-WinEvent -ComputerName $Workstation -FilterHashtable #{
Logname = 'system'
Id = '41', '1074', '1076', '6005', '6006', '6008', '6009', '6013'
StartTime = (Get-Date).AddDays(-$DaysFromToday)
} -MaxEvents $MaxEvents -ErrorAction Stop
foreach ($Event in $EventList) {
if ($Event.Id -eq 1074) {
[PSCustomObject]#{
TimeStamp = $Event.TimeCreated
Event = $Event.Id
ShutdownType = 'Restart'
UserName = $Event.Properties.value[6]
}
}
if ($Event.Id -eq 41) {
[PSCustomObject]#{
TimeStamp = $Event.TimeCreated
Event = $Event.Id
ShutdownType = 'Unexpected'
UserName = ' '
}
}
}
pause
}
}
}
until ($selection -eq 'q') #End of main menu
Works perfectly fine if I remove the script from the switch and run it separately, but as soon as I call it from the switch it still asks for the workstation/IP, how many days, and max events, but just outputs nothing.
Here is what it looks like when it works:
How many days would you like to go back?: 90
How many events would you like to view?: 999
TimeStamp Event ShutdownType UserName
--------- ----- ------------ --------
12/23/2022 12:20:55 AM 1074 Restart Username
12/20/2022 1:00:01 AM 1074 Restart Username
12/17/2022 12:21:54 AM 1074 Restart Username
12/13/2022 8:57:40 AM 1074 Restart Username
This is what I get when I run it within the switch menu
Workstation\IP Address(Use IP for remote users)?: IP Address
How many days would you like to go back?: 90
How many events would you like to view?: 999
Press Enter to continue...:
I have tried just doing 1 day and 1 event, but same results. No errors or anything indicating a failure, so not sure how to troubleshoot this. I have had similar issues with switches in the past that were resolved with some researching into scopes, but I don't think this is the same case as it is all self contained within the switch itself.
I am at a loss, any ideas? As always, any insight into my script is greatly appreciated, even if it doesn't resolve the problem at hand.
JosefZ has provided the crucial pointer:
force synchronous to-display output with, such as with Out-Host
if you neglect to do so, the pause statement will - surprisingly - execute before the [pscustomobject] instances emitted by the foreach statement, due to the asynchronous behavior of the implicitly applied Format-Table formatting - see this answer for details.
Here's a simplified example:
switch ('foo') {
default {
# Wrap the `foreach` statement in . { ... },
# so its output can be piped to Out-Host.
. {
foreach ($i in 1..3) {
[pscustomobject] #{ prop = $i }
}
} |
Out-Host # Without this, "pause" will run FIRST.
pause
}
}
Note:
For Out-Host to format all output together it must receive all output from the foreach loop as part of a single pipeline.
Since foreach is a language statement (rather than a command, such as the related ForEach-Object cmdlet) that therefore cannot directly be used at the start of a pipeline, the above wraps it in a script block ({ ... }) that is invoked via ., the dot-sourcing operator, which executes the script block directly in the caller's context and streams the output to the pipeline.
This limitation may be surprising, but is rooted in the fundamentals of PowerShell's grammar - see GitHub issue #10967.
An all-pipeline alternative that doesn't require the . { ... } workaround would be:
1..3 |
ForEach-Object {
[pscustomobject] #{ prop = $_ } # Note the automatic $_ var.
} |
Out-Host
What am I doing wrong here?
Why do the 2 variables not equal each other?
When I run this script
$temp1 = "#{Dhcp=Disabled}"
$temp2 = Get-NetIPInterface My_Ethernet | select Dhcp
write-host ""
write-host "1" $temp1
write-host "2" $temp2
write-host ""
if ($temp2 -eq $temp1){
write-host "IP address is Static "
}
Else {
write-host "IP address is Not Static"
}
I get this result
1 #{Dhcp=Disabled}
2 #{Dhcp=Disabled}
IP address is Not Static
With the helpful suggestion from Mathias this is now working as expected
$temp1 = "Disabled"
$temp2 = Get-NetIPInterface My_Ethernet | select Dhcp
write-host ""
write-host ""
write-host "1" $temp1
write-host "2" $temp2.dhcp
write-host ""
write-host ""
if ($temp2.dhcp -eq $temp1){
write-host "IP address is Static "
}
Else {
write-host "IP address is Not Static"
}
Just to complement your own effective solution:
Since your intent was to compare a property's value to another value, select -ExpandProperty Dhcp would have returned that value directly (see the docs for Select-Object):
if ((Get-NetIPInterface My_Ethernet | select -ExpandProperty Dhcp) -eq $temp1) { # ...
However, it would be much simpler to use direct property access, using ., the member-access operator:
if ((Get-NetIPInterface My_Ethernet).Dhcp -eq $temp1) { # ...
Note that .Dhcp would work even if your Get-NetIPInterface call returned multiple objects, in which case an array of values is returned, courtesy of PowerShell's member-access enumeration feature.[1]
Finally, note that Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g. $value, instead of Write-Host $value (or use Write-Output $value); see this answer.
To explicitly print only to the display but with rich formatting, use Out-Host, given that the .ToString() stringification that Write-Host uses on its input is often unhelpful - see this post.
[1] Note that PowerShell's comparison operators such as -eq exhibit filtering behavior when their LHS is an array (collection); that is, instead of returning $true or $false, they then return the sub-array of matching elements - see about_Comparison_Operators.
Abstract
So I work for a company that has roughly 10k computer assets on my domain. My issue is the time it takes to query if a user exists on a computer to see if they've ever logged into said computer. We need this functionality for audits in case they've done something they shouldn't have.
I have two methods in mind I've researched to complete this task, and a third alternative solution I have not thought of;
-Method A: Querying every computer for the "C:\Users<USER>" to see if LocalPath exists
-Method B: Checking every computer registry for the "HKU:<SID>" to see if the SID exists
-Method C: You are all smarter than me and have a better way? XD
Method A Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
ForEach($Computer in $AllADComputers) {
$CName = $Computer.Name
if (Get-CimInstance -ComputerName "$CName" -ClassName Win32_Profile | ? {"C:\Users\'$EDIPI'" -contains $_.LocalPath}) {
$AllCompFound += $CName
} else {
#DOOTHERSTUFF
}
}
NOTE: I have another function that prompts me to enter a username to check for. Where I work they are numbers so case sensitivity is not an issue. My issue with this function is I believe it is the 'if' statement returns true every time because it ran rather than because it matched the username.
Method B Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
$hive = [Microsoft:Win32.RegistryHive]::Users
ForEach($Computer in $AllADComputers) {
try {
$base = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hive, $Computer.Name)
$key = &base.OpenSubKey($strSID)
if ($!key) {
#DOSTUFF
} else {
$AllCompFound += $Computer.Name
#DOOTHERSTUFF
}
} catch {
#IDONTTHROWBECAUSEIWANTITTOCONTINUE
} finally {
if($key) {
$key.Close()
}
if ($base) {
$base.Close()
}
}
}
NOTE: I have another function that converts the username into a SID prior to this function. It works.
Where my eyes start to glaze over is using Invoke-Command and actually return a value back, and whether or not to run all of these queries as their own PS-Session or not. My Method A returns false positives and my Method B seems to hang up on some computers.
Neither of these methods are really fast enough to get through 10k results, I've been using smaller pools of computers in order to get test these results when requested. I'm by no means an expert, but I think I have a good understanding, so any help is appreciated!
First, use WMI Win32_UserProfile, not C:\Users or registry.
Second, use reports from pc to some database, not from server to pc. This is much better usually.
About GPO: If you get access, you can Add\Remove scheduled task for such reports through GPP (not GPO) from time to time.
Third: Use PoshRSJob to make parallel queries.
Get-WmiObject -Class 'Win32_USerProfile' |
Select #(
'SID',
#{
Name = 'LastUseTime';
Expression = {$_.ConvertToDateTime($_.LastUseTime)}}
#{
Name = 'NTAccount';
Expression = { [System.Security.Principal.SecurityIdentifier]::new($_.SID).Translate([System.Security.Principal.NTAccount])}}
)
Be careful with translating to NTAccount: if SID does not translates, it will cause error, so, maybe, it's better not to collect NTAccount from user space.
If you have no other variants, parallel jobs using PoshRSJob
Example for paralleling ( maybe there are some typos )
$ToDo = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() # This is Queue (list) of computers that SHOULD be processed
<# Some loop through your computers #>
<#...#> $ToDo.Enqueue($computerName)
<#LoopEnd#>
$result = [System.Collections.Concurrent.ConcurrentBag[Object]]::new() # This is Bag (list) of processing results
# This function has ComputerName on input, and outputs some single value (object) as a result of processing this computer
Function Get-MySpecialComputerStats
{
Param(
[String]$ComputerName
)
<#Some magic#>
# Here we make KSCustomObject form Hashtable. This is result object
return [PSCustomObject]#{
ComputerName = $ComputerName;
Result = 'OK'
SomeAdditionalInfo1 = 'whateverYouWant'
SomeAdditionalInfo2 = 42 # Because 42
}
}
# This is script that runs on background. It can not output anything.
# It takes 2 args: 1st is Input queue, 2nd is output queue
$JobScript = [scriptblock]{
$inQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]$args[0]
$outBag = [System.Collections.Concurrent.ConcurrentBag[Object]]$args[1]
$compName = $null
# Logging inside, if you need it
$log = [System.Text.StringBuilder]::new()
# we work until inQueue is empty ( then TryDequeue will return false )
while($inQueue.TryDequeue([ref] $compName) -eq $true)
{
$r= $null
try
{
$r = Get-MySpecialComputerStats -ComputerName $compName -EA Stop
[void]$log.AppendLine("[_]: $($compName) : OK!")
[void]$outBag.Add($r) # We append result to outBag
}
catch
{
[void]$log.AppendLine("[E]: $($compName) : $($_.Exception.Message)")
}
}
# we return log.
return $log.ToString()
}
# Some progress counters
$i_max = $ToDo.Count
$i_cur = $i_max
# We start 20 jobs. Dont forget to say about our functions be available inside job
$jobs = #(1..20) <# Run 20 threads #> | % { Start-RSJob -ScriptBlock $JobScript -ArgumentList #($ToDo, $result) -FunctionsToImport 'Get-MySpecialComputerStats' }
# And once per 3 seconds we check, how much entries left in Queue ($todo)
while ($i_cur -gt 0)
{
Write-Progress -Activity 'Working' -Status "$($i_cur) left of $($i_max) computers" -PercentComplete (100 - ($i_cur / $i_max * 100))
Start-Sleep -Seconds 3
$i_cur = $ToDo.Count
}
# When there is zero, we shall wait for jobs to complete last items and return logs, and we collect logs
$logs = $jobs | % { Wait-RSJob -Job $_ } | % { Receive-RSJob -Job $_ }
# Logs is LOGS, not result
# Result is in the result variable.
$result | Export-Clixml -Path 'P:/ath/to/file.clixml' # Exporting result to CliXML file, or whatever you want
Please be careful: there is no output inside $JobScript done, so it must be perfectly done, and function Get-MySpecialComputerStats must be tested on unusual ways to return value that can be interpreted.
Here is the code below I am using:
$ProcessesToCheckFor = (
'company_name_uat-Historian'
)
$FoundProcesses = Get-Process -Name $ProcessesToCheckFor -ErrorAction SilentlyContinue
foreach ($Item in $ProcessesToCheckFor)
{
if ($FoundProcesses.Name -contains $Item)
{
'{0} runn1ng' -f $Item
}
else
{
'{0} fai1ed' -f $Item
}
}
The code checks to see if company_name_uat-Historian process is running on a server, and if it is running, it will output runn1ng and fai1ed if not.
The problem is, it works when checking only one process just like the code above, but not when you try to check a list of processes.
I need to check a list of processes, so when I chain the rest as listed below:
$ProcessesToCheckFor = (
'company_name_uat-Historian',
'company_name_uat-KEReviewCollector',
'company_name_uat-lwm',
'company_name_uat-MQAck',
'company_name_uat-MQOutput',
'company_name_uat-SQAC',
'company_name_uat-Store',
'company_name_uat-Store_STS',
'company_name_uat-StoreLegacy',
'spotify'
)
$FoundProcesses = Get-Process -Name $ProcessesToCheckFor -ErrorAction SilentlyContinue
foreach ($Item in $ProcessesToCheckFor)
{
if ($FoundProcesses.Name -contains $Item)
{
'{0} runn1ng' -f $Item
}
else
{
'{0} fai1ed' -f $Item
}
They all output fai1ed.
If I do them one by one, each different process will return runn1ng, just all bunched up together they return fai1ed.
Side notes (if anyone is wondering):
runn1ng and fai1ed are 'code words' that get replaced by images using JavaScript. I am making an HTML dashboard that monitors my Windows servers for work, so think green checkmarks and red x marks and what not.
Using PowerShell version 2 is not my choice at all, some of the servers I am responsible for monitoring are Windows 2008 R2. I believe they are going to be updated sometime this year, but the project deliverable requires me to enact a monitoring solution immediately. There are also some servers which are 2012 that I love to write PowerShell scripts for.
spotify is listed as a process because I know it's a legit process but is not installed on our servers. When I was writing the script, I initially tested on my own personal machine and used spotify as a means to test runn1ng if I opened it or fai1ed if I closed it. If all my processes are runn1ng and spotify is fai1ed, then that's an indication that the PS code is working.
It appears to happen in PowerShell version 2.
Any ideas what is causing it and how I can rewrite it?
The Answer
if (($FoundProcesses | Select-Object -ExpandProperty Name) -contains $Item) {
Why
PowerShell 3 added something called member enumeration. If you have an array of objects in 2.0, you can't directly call properties of the array, because it looks for those properties on the array object itself. In 3.0+ if the member doesn't exist on the array, it will check the items for the member as well. Using Select-Object -ExpandProperty is a more explicit way of calling the members.
You could also just move the Get-Process call into the foreach loop.
foreach ($Item in $ProcessesToCheckFor)
{
if (Get-Process -Name $Item -ErrorAction SilentlyContinue)
{
'{0} runn1ng' -f $Item
}
else
{
'{0} fai1ed' -f $Item
}
}
Patrick Meinecke's answer is effective and helpful, but let me attempt a more detailed explanation and a more efficient solution:
First things first: Your code works just fine in PSv3+, which should provide an incentive to leave PSv2 behind.
PSv3 introduced unified handling of scalars and collections through member(-access) enumeration, which bridged the great scalar/array divide that plagued PSv2-.
Specifically, in PSv3 you needn't worry about whether Get-Process -Name $ProcessesToCheckFor happens to return a single or multiple items: in either case you can call .Count on the output to count the number of items returned and you can invoke a member (property) on the result, which, in the case of an array result (multiple items) means that the member is called on each element (e.g., .Name), and the result can be again be used as a scalar or array as needed.
In PSv2-, however, you need to be aware of whether a cmdlet - situationally - happens to return a single item or multiple ones, which means:
Use array-subexpression operator #(...) to ensure that even if the enclosed command happens to return a single item (scalar), the result is an array.
To collect the property values of the elements of a given array or of a scalar in a new array or scalar, use ... | Select-Object -ExpandProperty - again, apply #(...) to ensure that the result is an array.
Note that PSv2- had some unified scalar/array handling, but only with respect to pipeline processing: to send a scalar (single item) to the pipeline, you can use it as-is, without needing to wrap it in #(...):
'foo' | ... works just fine - no need for #('foo') | ... (even though that works too).
Applied to your code we get:
$ProcessesToCheckFor = (
'company_name_uat-Historian',
'company_name_uat-KEReviewCollector',
'company_name_uat-lwm',
'company_name_uat-MQAck',
'company_name_uat-MQOutput',
'company_name_uat-SQAC',
'company_name_uat-Store',
'company_name_uat-Store_STS',
'company_name_uat-StoreLegacy',
'spotify'
)
# Collect the names of all running processes and
# make sure the result is an array (`#(...)`).
$FoundProcesses = #(Get-Process -Name $ProcessesToCheckFor -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Name)
foreach ($Item in $ProcessesToCheckFor) {
if ($FoundProcesses -contains $Item) {
'{0} runn1ng' -f $Item
} else {
'{0} fai1ed' -f $Item
}
}
First off, I appoligize for my terminalogy. I self taught myself powershell, so my lingo may be a bit off.
I have a script that I am running in WinPE which brings up a menu system for windows imaging (selecting image file, running a diskpart file, ect.). Part of this script looks up which volumes are visible to windows PE and prompts the user to select which volume they would like to install the image to. I separated this portion of the code into it's own function. The issue I'm having is that the WMI call to lookup the logical disks returns blank results when the function is setting a variable (see example below). But when I dot-source and run the function by itself it works fine. Here is the code i'm using
$Vol = Select-VolumeLetter
---- In a seperate file which has been dot-sourced -----
Function Select-VolumeLetter {
$InstallVolumes = (gwmi Win32_LogicalDisk -namespace "root\CIMV2")
$i = 1
Write-Host "`nPlease Select a volume to install the OS on:`n"
ForEach ($V in $InstallVolumes) {
Write-Host "[$i]"
$V | Select Name, Size, FileSystem, VolumeName | fl
$i++
}
Do{
$Range = $i - 1
$Input = Read-Host "Choose 1-$Range"
$Result = ""
if(![int32]::TryParse( $Input , [ref]$Result )) {Write-Host "`nInvalid selection. Please try again.`n"}
} until ($Result -gt 0 -and $Result -lt ($i + 1))
$InstallVolumes[($Input -1)].Name
}
The result looks like:
[1]
[2]
[3] ect.
But when run just as a function I get the full WMI information requested in the correct place. Any help would be much appreciated.
So I think you have some terminology wrong. What you show is not the results, that it what appears on your screen due to the Write-Host calls. You are not seeing the WMI information because you are not specifying that it be written to the screen and it is therefore being passed back as a result of the function. In your case $Vol will contain that WMI information. I think what would fix your situation would be something like this:
$InstallVolumes = (gwmi Win32_LogicalDisk -namespace "root\CIMV2")
$i = 1
Write-Host "`nPlease Select a volume to install the OS on:"
ForEach ($V in $InstallVolumes) {
Write-Host "`n[$i]"
Write-Host ($V | Select Name, Size, FileSystem, VolumeName | fl|out-string).trim()
$i++
}