Script won't run in Switch menu - powershell

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

Related

Powersell - Remotely Query if User Exists Across Domain [Fastest]

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.

How can I escape a read-host by pressing escape?

just wondering if its possible to escape a read-host in a while loop by pressing escape.
I've tried doing an do-else loop but it will only recognize button presses outside of the read-host.
This is basically what I have
#Import Active Directory Module
Import-Module ActiveDirectory
#Get standard variables
$_Date=Get-Date -Format "MM/dd/yyyy"
$_Server=Read-Host "Enter the domain you want to search"
#Request credentials
$_Creds=Get-Credential
while($true){
#Requests user input username
$_Name=Read-Host "Enter account name you wish to disable"
#rest of code
}
I want to be able to escape it if I want to change the domain
Using Read-Host you cannot do this, but you might consider using a graphical input dialog instead of prompting in the console. After all, the Get-Credential cmdlet also displays a GUI.
If that is an option for you, it can be done using something like this:
function Show-InputBox {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Message,
[string]$Title = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.PSCommandPath),
[string]$defaultText = ''
)
Add-Type -AssemblyName 'Microsoft.VisualBasic'
return [Microsoft.VisualBasic.Interaction]::InputBox($Message, $Title, $defaultText)
}
while($true) {
$_Name = Show-InputBox "Enter account name you wish to disable"
if ([string]::IsNullOrWhiteSpace($_Name)) {
# the box was cancelled, so exit the loop
break
}
# proceed with the rest of the code
}
If the user presses the Esc key, clicks Cancel, or leaves the input blank, you can exit the while loop, otherwise proceed with the code.
You cannot do it with Read-Host, but you can do it via the PSReadLine module (ships with Windows PowerShell version 5 or higher on Windows 10 / Windows Server 2016) and PowerShell Core) and its PSConsoleHostReadline function:
Important:
As of PSReadLine v2.0.0-beta3, the solution below is a hack, because the PSConsoleHostReadline only supports prompting for PowerShell statements, not open-ended user input.
This GitHub feature suggestion asks for the function to be optionally usable for general-purpose user input as well, which would allow for a greatly customizable end-user prompting experience. Make your voice heard there, if you'd like see this suggestion implemented.
The hack should work in your case, since the usernames you're prompting for should be syntactically valid PowerShell statements.
However, supporting arbitrary input is problematic for two reasons:
Inapplicable syntax coloring will be applied - you could, however, temporarily set all configurable colors to the same color, but that would be cumbersome.
More importantly, if an input happens to be something that amounts to a syntactically incomplete PowerShell statement, PSConsoleHostReadline won't accept the input and instead continue to prompt (on a new line); for instance, input a| would cause this problem.
Also:
Whatever input is submitted is invariably added to the command history.
While you can currently remove a temporarily installed keyboard handler on exiting a script, there is no robust way to restore a previously active one - see this GitHub issue.
# Set up a key handler that cancels the prompt on pressing ESC (as Ctrl-C would).
Set-PSReadLineKeyHandler -Key Escape -Function CancelLine
try {
# Prompt the user:
Write-Host -NoNewline 'Enter account name you wish to disable: '
$_Name = PSConsoleHostReadLine
# If ESC was pressed, $_Name will contain the empty string.
# Note that you won't be able to distinguish that from the user
# just pressing ENTER.
$canceled = $_Name -eq ''
# ... act on potential cancellation
} finally {
# Remove the custom Escape key handler.
Remove-PSReadlineKeyHandler -Key Escape
}
I wrote this function that works for me.
# Returns the string "x" when Escape key is pressed, or whatever is indicated with -CancelString
# Pass -MaxLen n to define max string length
function Read-HostPlus()
{
param
(
$CancelString = "x",
$MaxLen = 60
)
$result = ""
$cursor = New-Object System.Management.Automation.Host.Coordinates
while ($true)
{
While (!$host.UI.RawUI.KeyAvailable ){}
$key = $host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown")
switch ($key.virtualkeycode)
{
27 { While (!$host.UI.RawUI.KeyAvailable ){}; return $CancelString }
13 { While (!$host.UI.RawUI.KeyAvailable ){}; return $result }
8
{
if ($result.length -gt 0)
{
$cursor = $host.UI.RawUI.CursorPosition
$width = $host.UI.RawUI.MaxWindowSize.Width
if ( $cursor.x -gt 0) { $cursor.x-- }
else { $cursor.x = $width -1; $cursor.y-- }
$Host.UI.RawUI.CursorPosition = $cursor ; write-host " " ; $Host.UI.RawUI.CursorPosition = $cursor
$result = $result.substring(0,$result.length - 1 )
}
}
Default
{
$key_char = $key.character
if( [byte][char]$key_char -ne 0 -and [byte][char]$key_char -gt 31 -and ($result + $key_char).Length -le $MaxLen )
{
$result += $key_char
$cursor.x = $host.UI.RawUI.CursorPosition.X
Write-Host $key_char -NoNewline
if ($cursor.X -eq $host.UI.RawUI.MaxWindowSize.Width-1 ) {write-host " `b" -NoNewline }
}
}
}
}
}

How to configure a timeout for Read-Host in PowerShell

Like I said, this code works in PowerShell version 2, but not in PowerShell version 5.
function wait
{
$compte = 0
Write-Host "To continue installation and ignore configuration warnings type [y], type any key to abort"
While(-not $Host.UI.RawUI.KeyAvailable -and ($compte -le 20))
{
$compte++
Start-Sleep -s 1
}
if ($compte -ge 20)
{
Write-Host "Installation aborted..."
break
}
else
{
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyup")
}
if ($key.character -eq "y")
{Write-Host "Ignoring configuration warnings..."}
else
{Write-Host "Installation aborted..."
}}
The official documentation or Read-Host -? will tell that it's not possible to use Read-Host in that manner. There is no possible parameter to tell it to run with some kind of timeout.
But there are various other questions detailing how to do this in PowerShell (usually utilizing C#).
The idea seems to be to check whenever the user pressed a key using $Host.UI.RawUI.KeyAvailable and check that for the duration of your timeout.
A simple working example could be the following:
$secondsRunning = 0;
Write-Output "Press any key to abort the following wait time."
while( (-not $Host.UI.RawUI.KeyAvailable) -and ($secondsRunning -lt 5) ){
Write-Host ("Waiting for: " + (5-$secondsRunning))
Start-Sleep -Seconds 1
$secondsRunning++
}
You could use $host.UI.RawUI.ReadKey to get the key that was pressed. This solution probably would not be acceptable if you need more complex input than a simple button press. See also:
Windows PowerShell Tip of the Week - Pausing a Script Until the User Presses a Key
PowerTip: Use PowerShell to Wait for a Key Press (Hey, Scripting Guy!)
Seth, thank you for your solution. I expanded on the example you provided and wanted to give that back to the community.
The use case is a bit different here - I have a loop checking if an array of VMs can be migrated and if there are any failures to that check the operator can either remediate those until the checks clear or they can opt to "GO" and have those failing VMs excluded from the operation. If something other than GO is typed state remains within the loop.
One downside to this is if the operator inadvertently presses a key the script will be blocked by Read-Host and may not be immediately noticed. If that's a problem for anyone I'm sure they can hack around that ;-)
Write-Host "Verifying all VMs have RelocateVM_Task enabled..."
Do {
$vms_pivoting = $ph_vms | Where-Object{'RelocateVM_Task' -in $_.ExtensionData.DisabledMethod}
if ($vms_pivoting){
Write-Host -ForegroundColor:Red ("Some VMs in phase have method RelocateVM_Task disabled.")
$vms_pivoting | Select-Object Name, PowerState | Format-Table -AutoSize
Write-Host -ForegroundColor:Yellow "Waiting until this is resolved -or- type GO to continue without these VMs:" -NoNewline
$secs = 0
While ((-not $Host.UI.RawUI.KeyAvailable) -and ($secs -lt 15)){
Start-Sleep -Seconds 1
$secs++
}
if ($Host.UI.RawUI.KeyAvailable){
$input = Read-Host
Write-Host ""
if ($input -eq 'GO'){
Write-Host -ForegroundColor:Yellow "NOTICE: User prompted to continue migration without the blocked VM(s)"
Write-Host -ForegroundColor:Yellow "Removing the following VMs from the migration list"
$ph_vms = $ph_vms | ?{$_ -notin $vms_pivoting} | Sort-Object -Property Name
}
}
} else {
Write-Host -ForegroundColor:Green "Verified all VMs have RelocateVM_Task method enabled."
}
} Until(($vms_pivoting).Count -eq 0)
Also note that all this $Host.UI stuff doesn't work from the Powershell ISE.
To find out from within a script you could test for $Host.Name -eq "ConsoleHost". When true you can use the code from this topic. Otherwise you could use $Host.UI.PromptForChoice or any other way of showing a dialog box. With System.Windows.Forms.Timer you can then set a timer, and code to close the dialog box or form can be run when it expires.

Powershell - Checking Security Group Members against another list

A bit of context, I am trying to get a list of users from a security group, then check if any of those users do not have an assigned XenDesktop.
Please forgive me, I only started using Powershell yesterday so my formatting is off. It first grabs the users from the AD group then checks if that user has an assigned desktop, which works but I what I can't seem to get working is that if the user is found in that list, I want it to move onto the next username, instead it continues to check against every machine and then onto the final bits of code.
$checkusernames = Get-ADGroupMember "***AD security Group***" | Select SamAccountName
$desktops = get-brokerdesktop -DesktopGroupName Personal_WIN8 | Select MachineName, #{Name='AssociatedUserNames';Expression={[string]::join(“;”, ($_.AssociatedUserNames))}}
foreach ($username in $checkusernames.SamAccountName) {
foreach ($desktop in $desktops) {
If ($desktop.AssociatedUserNames -like "*$username*") {
write-host $username "is assigned to" $desktop.machinename
}
write-host $username "is not assigned to a desktop"
}
Write-host $username "is not assigned to anything"
pause
}
If you want to exit from a ForEach loop early you can do so with the Break keyword. For example:
$checkusernames = Get-ADGroupMember "***AD security Group***" | Select SamAccountName
$desktops = get-brokerdesktop -DesktopGroupName Personal_WIN8 | Select MachineName, #{Name='AssociatedUserNames';Expression={[string]::join(“;”, ($_.AssociatedUserNames))}}
foreach ($username in $checkusernames.SamAccountName) {
$machine = ''
foreach ($desktop in $desktops) {
If ($desktop.AssociatedUserNames -like "*$username*") {
$machine = $desktop.machinename
break
}
}
If ($machine) {
write-host $username "is assigned to" $desktop.machinename
} Else {
write-host $username "is not assigned to a desktop"
pause
}
}
This will stop the current cycle of the inner ForEach loop (once it has completed in it's entirety) without interrupting the ongoing cycle of the outer one.
Per the dicussion in the comments, i've also reorganised the code so that you only get a single output dependent on whether a desktop is matched to a user or not and it only pauses if it does not find a match.

How can I implement Get-Credential with a timeout

In some of my scripts I have a fall-back approach for getting a PSCredential object via the Get-Credential cmdlet. This is very useful when running those scripts interactively, esp. during testing etc.
If & when I run these scripts via a task scheduler, I'd like to ensure that they don't get stuck waiting for interactive input and simply fail if they don't have the necessary credentials. Note that this should never happen if the task is set to run under the correct application account.
Does anyone know of an approach that would allow me to prompt for input with a timeout (and presumably a NULL credential object) so the script doesn't get stuck?
Happy to consider a more general case with Read-Host instead of Get-Credential.
I use something similar in some of my scripts based around a timeout on read-host, code here :
Function Read-HostTimeout {
# Description: Mimics the built-in "read-host" cmdlet but adds an expiration timer for
# receiving the input. Does not support -assecurestring
# Set parameters. Keeping the prompt mandatory
# just like the original
param(
[Parameter(Mandatory=$true,Position=1)]
[string]$prompt,
[Parameter(Mandatory=$false,Position=2)]
[int]$delayInSeconds=5,
[Parameter(Mandatory=$false,Position=3)]
[string]$defaultValue = 'n'
)
# Do the math to convert the delay given into milliseconds
# and divide by the sleep value so that the correct delay
# timer value can be set
$sleep = 250
$delay = ($delayInSeconds*1000)/$sleep
$count = 0
$charArray = New-Object System.Collections.ArrayList
Write-host -nonewline "$($prompt): "
# While loop waits for the first key to be pressed for input and
# then exits. If the timer expires it returns null
While ( (!$host.ui.rawui.KeyAvailable) -and ($count -lt $delay) ){
start-sleep -m $sleep
$count++
If ($count -eq $delay) { "`n"; return $defaultValue}
}
# Retrieve the key pressed, add it to the char array that is storing
# all keys pressed and then write it to the same line as the prompt
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp").Character
$charArray.Add($key) | out-null
# Comment this out if you want "no echo" of what the user types
Write-host -nonewline $key
# This block is where the script keeps reading for a key. Every time
# a key is pressed, it checks if it's a carriage return. If so, it exits the
# loop and returns the string. If not it stores the key pressed and
# then checks if it's a backspace and does the necessary cursor
# moving and blanking out of the backspaced character, then resumes
# writing.
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
While ($key.virtualKeyCode -ne 13) {
If ($key.virtualKeycode -eq 8) {
$charArray.Add($key.Character) | out-null
Write-host -nonewline $key.Character
$cursor = $host.ui.rawui.get_cursorPosition()
write-host -nonewline " "
$host.ui.rawui.set_cursorPosition($cursor)
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
}
Else {
$charArray.Add($key.Character) | out-null
Write-host -nonewline $key.Character
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyUp")
}
}
Write-Host ""
$finalString = -join $charArray
return $finalString
}