Watch-PerfCounter

Just a little Powershell I wrote when I had nothing better to do.  It's kinda' reminiscent of procdump's ability to wait until a certain threshold is crossed before it takes action.  That's what this Cmdlet does.  It just watches a performance counter (any performance counter, also works against remote computers,) and when the specified threshold is crossed, it executes whatever file you specified in the "Action" parameter.  I wrote it because it helps solve performance-related cases where a repro is hard to catch.

#
<#
.SYNOPSIS
Watches a Windows performance counter, and executes the specified file
once the specified counter threshold is crossed.
.DESCRIPTION
Watches a Windows performance counter, and executes the specified file
once the specified counter threshold is crossed. You must know the 
name and path of the performance counter you're after. Use Get-Counter
if you want to explore the syntax of performance counter paths. You
may monitor performance counters on a remote machine as well. The
specified Action can be any executable file, an exe, a vbs, a bat file,
etc. You can also specify the PollFrequency, in seconds. Default is 5.
Use the Verbose switch for extra detail.
.PARAMETER CounterName
This is the full path of the performance counter that you want to monitor.
It may be from the local machine or from a remote machine.
.PARAMETER Threshold
When the performance counter crosses this threshold, the specified action will be triggered. 
It could be an absolute value or it could be a percentage, depending on the perf counter.
.PARAMETER Action
This can be any file that exists and is accessible - it will be executed when the performance
counter threshold is crossed.
.PARAMETER PollFrequency
The frequency, in seconds, that the performance counter will be polled. Default is 5 seconds.
.EXAMPLE
C:\PS> Watch-PerfCounter -CounterName '\processor(_total)\% processor time' -Threshold 90 -Action 'C:\MyFile.bat' -PollFrequency 10 -Verbose
Polls the processer time performance counter every 10 seconds, and executes MyFile.bat once the CPU is over 90%. Outputs Verbose information.
.EXAMPLE
C:\PS> Watch-PerfCounter -CounterName '\\host02\memory\% committed bytes in use' -Threshold 25 -Action 'C:\Compress.exe'
Polls the memory perf counter on remote computer HOST02, and executes a program once it crosses 25%.
.NOTES
Powershell written by Joseph Ryan Ries, but it was Justin Turner's idea.
#>
Function Watch-PerfCounter
{
    #Version: 01.00 - September 17 2015 - Initial release.
    #Version: 01.01 - September 17 2015 - Added the word "seconds" to the sentence "Polling ever x seconds."
    [CmdletBinding()]
    Param([Parameter(Mandatory=$True)]
            [String]$CounterName,
          [Parameter(Mandatory=$True)]
            [Decimal]$Threshold,
          [Parameter(Mandatory=$True)]
          [ValidateScript({Test-Path $_ -PathType Leaf})]
            [String]$Action,
          [Parameter(Mandatory=$False)]
          [ValidateRange(1,360)]
            [Int]$PollFrequency = 5)
    BEGIN
    {
        Set-StrictMode -Version Latest
        [Diagnostics.Stopwatch]$Stopwatch = [Diagnostics.Stopwatch]::StartNew()

        Write-Verbose "$($PSCmdlet.CommandRuntime) beginning on $(Get-Date)."

        Try
        {
            Get-Counter $CounterName -ErrorAction Stop | Out-Null
            Write-Verbose "Performance counter $CounterName was found."
        }
        Catch
        {
            Write-Error "Perfermance counter was not found or could not be read! `n`n $($_.Exception.Message)"
            Return
        }

        Write-Verbose "Polling every $PollFrequency seconds."
        Write-Verbose "Use Ctrl+C to abort."


        [Bool]$ThresholdCrossed = $False

        While (-Not($ThresholdCrossed))
        {
            Try
            {
                $CookedValue = (Get-Counter $CounterName -ErrorAction Stop).CounterSamples.CookedValue

                Write-Verbose "$CounterName = $CookedValue"

                If ($CookedValue -GT $Threshold)
                {
                    Write-Verbose "Performance counter $CounterName has crossed the threshold of $Threshold!"
                    $ThresholdCrossed = $True
                }
            }
            Catch
            {
                Write-Error "Error reading performance counter. This error may be transient. `n`n $($_.Exception.Message)"
            }

            If (-Not($ThresholdCrossed))
            {
                Start-Sleep -Seconds $PollFrequency
            }
        }

        Invoke-Expression $Action
    }
    PROCESS
    {

    }
    END
    {
        # Reminder: This code block still executes even if we return prematurely from the BEGIN block.
        $Stopwatch.Stop()
        Write-Verbose "$($PSCmdlet.CommandRuntime) completed in $([Math]::Round($Stopwatch.Elapsed.TotalSeconds, 2)) seconds."
    }
}

Some DFS Diagnostics and How to Make Dfsutil.exe Portable

There was a question recently about whether DFS was setup and working properly.

Well, let me back up.  There was a problem where an application dumped a file into a file share, but for some reason, the users were unable to see that file for up to an hour after it had supposedly been dumped into the file share.  And that file share was part of a DFS replication group.  So maybe the users were having to wait on DFS replication for some reason.  DFS was suspected, but we were short on evidence and facts.

So the users were in another geographical location, in another Active Directory site.  I wanted to see which DFS replication partner was the active target from the perspective of those clients in the other site.  For example, think of when you look at the following dialog box:


All well and good, but I needed to do this through the command line... via remote administration.

And the client was Windows 7, and it did not have Powershell remoting enabled, nor did it have RSAT (Remote Server Admin Tools) installed.

So if I wanted to check this DFS information on my own machine, which does have RSAT installed, I'd simply type:

C:\> dfsutil /PktInfo

And that will dump out lots of historical DFS target info like so:

C:\> dfsutil /PktInfo
12 entries...
Entry: \Server01\SiteB\Files
ShortEntry: \Server01\SiteB\Files
Expires in 89 seconds
UseCount: 0 Type:0x1 ( DFS )
   0:[\Server02\Legacy] AccessStatus: 0 ( ACTIVE TARGETSET )
   1:[\Server03\DFSRoot\Accounting\SiteB\Files] ( TARGETSET )
Entry: \Server01\SiteB\Files ShortEntry: \Server01\SiteB\Files Expires in 97 seconds UseCount: 0 Type:0x1 ( DFS ) 0:[\Server02\Files] AccessStatus: 0 ( ACTIVE TARGETSET ) 1:[\Server03\DFSRoot\Accounting\SiteB\Files] ( TARGETSET )

And so on... but I need to see things from the perspective of a client at that other site.

So the first thing I will do is... use psexec to enable Powershell Remoting on one of the remote clients!

psexec \\pc01 powershell.exe -Command "Enable-PSRemoting -Force"

Why? Because I will bootstrap you into the new decade kicking and screaming if I have to, that's why!

Secondly, I need to transfer dfsutil.exe (and dependencies) to the destination PC.  I don't want to install the entire RSAT on the remote client just to run this single test. The only hitch here is that you must use the version of dfsutil.exe that comes from the Remote Server Administration Tools that was meant for that version of Windows. So for instance, I couldn't transfer my version of dfsutil.exe to the destination PC, because the destination PC was Windows 7 and my workstation was Windows 8.1. I had to find a Windows 7 workstation with RSAT already installed as the source.  Also, you need to transfer the language file, dfsutil.exe.mui, into System32\en-US\.  So:

dfsutil.exe     -> \\pc01\c$\windows\system32\dfsutil.exe
dfsutil.exe.mui -> \\pc01\c$\windows\system32\en-US\dfsutil.exe.mui

Now I was ready to use Enter-PSSession to start a remote session on the PC and use dfsutil to run the diagnostic test.

BausButton

I'm back, and I brought along with me my newest creation: BausButton. (Pronounced Boss Button.)

This little Windows app sits innocuously in your notification tray.  When you hit the hotkey, whatever window has focus at that moment disappears.  The window disappears and the task bar icon (if any) also disappears. Hit the hotkey again and the window reappears.  See... a boss button... for when the boss walks in and you need to quickly hide whatever you were looking at.

But there's more. It's got password protection. If you have set a password previously, then BausButton will challenge you before it un-hides the hidden window.  Passwords are salted and then hashed with SHA256. (A new random salt is generated upon each new "installation" of the app.)

By default, the "boss button" is mapped to the "Scroll Lock" key, but you may customize the hotkey by editing the BausButton registry key.

 

The "HideKey" value corresponds to the virtual key-codes defined by Microsoft, which you can find right here. So for instance, if you wanted to change the hotkey to the space bar, you'd change the HideKey registry value to 0x20 (decimal 32.)

If you ever just want to reset everything, just close the app, delete the whole BausButton registry key, then restart the app.

Some screenshots:



When no window is hidden, the BausButton icon is colored in.



When a window is hidden, the BausButton icon is transparent.


If you click the icon, you get a very simple menu. If you exit the app, any hidden window will automatically be unhidden before the app exits, and if a password is set, you will be challenged for that password before the app exits, and if you get the password wrong, the app just won't exit.

Here's the signed binary. Let me know what you think.

(Update 8/14/2015: v1.0.1 - Fixed password dialog focus problem.)

BausButton.zip (144 KB)

Universal Pause Button

I like to play video games.  I also have a significant other, and she often walks into the room to talk to me while I'm playing a video game.  I would like to pause the game so that I can give her my undivided attention while she's talking to me, but a lot of games (particularly single-player ones) have these "un-pausable" cut scenes or other areas of the game where the normal pause functionality doesn't work.  This annoys both me and her, because I'm supposed to be the computer expert, and it looks like I don't even know how to pause my stupid video game.  So usually what ends up happening is I skip the cut scene and miss the story, or upset my SO by not paying attention to her as well as I should.

So that is why I wrote Universal Pause Button. It's a very simple Windows desktop app that sits in the system tray. Its icon resembles a pause button.  When you hit the actual Pause key (also known as Break) on your keyboard, the program determines which window is currently in the foreground (i.e. your game's window,) and pauses it.  No matter where you are in the game. Even in the middle of one of those pesky cut scenes that would otherwise be un-pausable.  When you press the key again (as long as you haven't since re-focused to another window,) the game will un-pause.

As of v1.0.3 you can now customize the "Pause" key that you want to use. Read the settings.txt file. The program reads the custom pause key from the settings.txt file during startup.

I've currently been testing this with The Witcher 3, and it is working great.  However, your mileage may vary. "Pausing" processes is something that usually only debuggers do, and I can't predict how your game will react to it.  Pausing processes may lead to race conditions among the threads of that process, but like I said, testing has been very positive for me so far.  I've already gotten great value out of the program, as there are lots of cut scenes in The Witcher 3, that I don't want to skip. The main use case for this app is single player games, as pausing your multi-player game will undoubtedly just get you kicked from the session, as if your computer had just crashed or hung. So don't use it in multi-player games.  It also works on applications that are not games at all.

Also, it's open source!  You can find the source on Github here.

If you prefer to just download the (signed) executable, here it is:

UniversalPauseButton.exe (124.3KB)

Get-Quotation: Something For Your Powershell Profile

I was playing around with my Powershell profile (again,) and I wanted to put a message-of-the-day style gizmo in there.  Now, a random quotation greets me every time I open Powershell:

  • Where do the quotes come from?

The function downloads them from www.quotationspage.com using Invoke-WebRequest.

  • What if I am not connected to the internet?

The function locally stores each unique quotation that it downloads. In the event that you are disconnected from the internet, the function will simply draw one of the locally cached quotes at random.

  • Where is my Powershell profile stored?

$Env:HOMEPATH\Documents\WindowsPowershell\Microsoft.Powershell_profile.ps1

  • Can I use this outside of my Powershell profile?

Sure.  It is just a function named Get-Quotation.

Here it is:

(Edit: A couple hours later, added wordwrap)

#
Function Get-Quotation
{
    Set-StrictMode -Version Latest

    $Form = @{'number'='1'; 
              'collection[0]'  = 'devils';
              'collection[1]'  = 'mgm';
              'collection[2]'  = 'motivate';
              'collection[3]'  = 'classic';
              'collection[4]'  = 'coles';
              'collection[5]'  = 'lindsly';
              'collection[6]'  = 'poorc';
              'collection[7]'  = 'altq';
              'collection[8]'  = '20thcent';
              'collection[9]'  = 'bywomen';
              'collection[10]' = 'contrib'}
    
    [String[]]$FormattedQuote = @()
    [Int]$MaxWidth = 0
    If ($Host.Name -EQ 'ConsoleHost')
    {
        $MaxWidth = $Host.UI.RawUI.WindowSize.Width
    }
    Else
    {
        $MaxWidth = 80
    }

    Try
    {
        $Page = Invoke-WebRequest http://www.quotationspage.com/random.php3 -Method Post -ContentType 'application/x-www-form-urlencoded' -Body $Form -ErrorAction Stop -TimeoutSec 5 -MaximumRedirection 0

        Foreach ($Element In $Page.AllElements)
        {
            If ($Element.tagName -EQ 'DL')
            {
                [String[]]$PreFormattedQuote = $Element.outerText -Split [Environment]::NewLine                

                For ($Index = 0; $Index -LT $PreFormattedQuote.Count; $Index++)
                {
                    If (($PreFormattedQuote[$Index].Length -GT 0) -AND -Not($PreFormattedQuote[$Index].Contains('More quotations on:')))
                    {
                        $FormattedQuote += $PreFormattedQuote[$Index]
                    }                    
                }
                $FormattedQuote[-1] = "`t-- $($FormattedQuote[-1])"                
            }
        }

        $Hasher = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
        $Hashed = $Hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($FormattedQuote[0]))
        [String]$HashString = [BitConverter]::ToString($Hashed).Replace('-', $Null)

        If (-Not(Test-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') -PathType Container))
        {
            New-Item (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') -ItemType Directory | Out-Null
        }

        If (-Not(Test-Path (Join-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') $HashString) -PathType Leaf))
        {            
            $FormattedQuote | Out-File (Join-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') $HashString)
        }
    }
    Catch
    {
        Write-Warning "Failed to get quotation from www.quotationspage.com. ($($_.Exception.Message))"

        $FormattedQuote = Get-Content ((Get-ChildItem (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') | Get-Random).FullName)
    }

    # Word wrap!
    [Int]$Column = 0
    Foreach ($Line in $FormattedQuote)
    {
        If ($FormattedQuote.IndexOf($Line) -EQ ($FormattedQuote.Count -1))
        {
            Write-Host "`n$Line" -ForegroundColor DarkGray
            Continue
        }
        
        [String[]]$Words = $Line -Split ' '
        Foreach ($Word In $Words)
        {
            # Strip any control characters from the word.
            $Word = $Word.Replace('`r', $Null).Replace('`n', $Null).Replace('`t', $Null)

            $Column += $Word.Length + 1   
            If ($Column -GT ($MaxWidth - 8))
            {
                Write-Host
                $Column = 0
            }
            Write-Host "$Word " -NoNewline -ForegroundColor DarkCyan            
        }
        
    }
    Write-Host
}

Get-Quotation