PowerShell Cookbook

Search

Categories

 

On this page

More Tied Variables in PowerShell
Want to Influence the PowerShell Cookbook V2?
Moving and Deleting Really Locked Files in PowerShell
Making Perfect Change with the Fewest Coins
More PowerShell Syntax Highlighting
PowerShell Script Encrypter
Scripting WinDbg with PowerShell
PowerShell P/Invoke Walkthrough
DateTime Casts are Language Primitives
Should I Refinance? PowerShell the Financial Advisor

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 262
This Year: 13
This Month: 0
This Week: 0
Comments: 818

Sign In

 Thursday, March 26, 2009
Thursday, March 26, 2009 2:51:52 PM (Pacific Standard Time, UTC-08:00) ( )

Ibrahim just posted something to the PowerShell blog about how to create tied variables in PowerShell. If you extend this approach with script blocks, you have a very powerful dynamic scripting technique.

PS C:\temp> cd \temp
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:lee { $myTestVariable } { $GLOBAL:myTestVariable = 2 * $args[0] }
PS C:\temp> $lee
PS C:\temp> $lee = 10
PS C:\temp> $lee
20
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:today { (Get-Date).DayOfWeek }
PS C:\temp> $today
Wednesday
PS C:\temp> New-ScriptVariable.ps1 GLOBAL:random -Get { Get-Random } -Set { Get-Random -SetSeed $args[0] }
PS C:\temp> $random
1740776676
PS C:\temp> $random
1507521897
PS C:\temp> $random = 10
PS C:\temp> $random
1613858733
PS C:\temp> $random = 10
PS C:\temp> $random
1613858733

He alluded to it in the post – here is the full text of the script:

(Edit 05/17: Updated to make the getters more like PowerShell pipelines: return a single object, or collection of PSObject)

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
## New-ScriptVariable.ps1
param($name, [ScriptBlock] $getter, [ScriptBlock] $setter)

Add-Type @"
using System;
using System.Collections.ObjectModel;
using System.Management.Automation;

namespace Lee.Holmes
{
    public class PSScriptVariable : PSVariable
    {
        public PSScriptVariable(string name,
            ScriptBlock scriptGetter, ScriptBlock scriptSetter)
            : base(name, null, ScopedItemOptions.AllScope)
        {
            getter = scriptGetter;
            setter = scriptSetter;
        }
        private ScriptBlock getter;
        private ScriptBlock setter;

        public override object Value
        {
            get
            {
                if(getter != null)
                {
                    Collection<PSObject> results = getter.Invoke();
                    if(results.Count == 1)
                    {
                        return results[0];
                    }
                    else
                    {
                        PSObject[] returnResults = new PSObject[results.Count];
                        results.CopyTo(returnResults, 0);
                        return returnResults;
                    }
                }
                else { return null; }
            }
            set
            {
                if(setter != null) { setter.Invoke(value); }
            }
        }
    }
}
"@


if(Test-Path variable:\$name)
{
    Remove-Item variable:\$name -Force
}
$executioncontext.SessionState.PSVariable.Set(
    (New-Object Lee.Holmes.PSScriptVariable $name,$getter,$setter))

 

Comments [5] | | # 
 Wednesday, March 11, 2009
Wednesday, March 11, 2009 9:41:25 PM (Pacific Standard Time, UTC-08:00) ( )

We've started working on the next edition of the PowerShell Cookbook, and one obvious goal is to improve on the first version.

As the first version has been in print, I've taken notes on where people get confused with certain recipes. I've taken notes on what I felt were content gaps, and taken the feedback from reviews on Amazon.com and random blogs. Reviews on Amazon are GOLD for authors. They help readers form educated opinions, and provide helpful feedback about the book itself. If you want to thank the author of a book you like, write a review on Amazon.

The second edition of the PowerShell Cookbook continues in the same tradition as the first. Topical, real-world solutions to everyday problems. Packed with an appendix of reference material that matters. It will continue to be a purposefully distinct approach from PowerShell in Action.

With that, here's your chance to influence the next edition. What did you find too basic? Too advanced? Missing altogether? Were there any recurring issues with the approach or content?

Another question we're pondering is the unique value that the printed edition brings to the table. Much of the content in the PowerShell Cookbook was pre-published to this blog, newsgroups, or other channels. Many of the topics it addresses can be found through internet searches and forums. Many copies are floating around on Bit Torrent. Given all of that, why did you still purchase the printed version?

I know -- a lot of questions, very few answers! Let 'er rip.

Comments [6] | | # 
 Tuesday, February 17, 2009
Tuesday, February 17, 2009 3:55:19 PM (Pacific Standard Time, UTC-08:00) ( )

Once in awhile, you need to do brain surgery on files locked by the system. This is a common problem run into by patches and hotfixes, so Windows has a special mechanism that lets it move files before any process has the chance to get its grubby little hands on it. This can only be done during a reboot, leading to the dire warning given to you by many installers.

The Win32 API that enables this is MoveFileEx. Calling this API with the MOVEFILE_DELAY_UNTIL_REBOOT flag tells Windows to move (or delete) your file at the next boot.

Here’s how to do it from PowerShell:

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024

function Move-LockedFile
{
    param($path, $destination)

    $path = (Resolve-Path $path).Path
    $destination = $executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($destination)

    $MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004

    $memberDefinition = @'
    [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
    public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName,
       int dwFlags);
'@


    $type = Add-Type -Name MoveFileUtils -MemberDefinition $memberDefinition -PassThru
    $type::MoveFileEx($path, $destination, $MOVEFILE_DELAY_UNTIL_REBOOT)
}

[C:\Windows\system32\config\txr]
PS:181 > dir -force | % { Move-LockedFile $_.Name (Join-Path c:\temp\txr ($_.Name + ".Bak")) }

[C:\Users\leeholm]
PS:182 > dir -Filter "NTUser.DAT{*" -force | % { Move-LockedFile $_.Name (Join-Path c:\temp\txr ($_.Name + ".Bak")) }

Comments [2] | | # 
 Monday, February 09, 2009
Monday, February 09, 2009 9:16:40 PM (Pacific Standard Time, UTC-08:00) ( )

I've long wondered exactly how few coins you need in your pocket in order to perfectly round out any bill. I usually grab a handful and hope it works out. Even that mathematically astute technique sometimes leaves me a nickel or few pennies short, though, so I settle for making change that gets me a quarter back instead of yet another handful of ore.

Even this settle-for-second-best option isn't that great. It can cause permanent damage to unsuspecting cashiers that aren't so good at math. Wondering why you would ever give them $1.13 for a $0.88 bill, they'll often just give you your change back AND then the stack of coins they were originally planning to load you up with.

Well, no longer.

It turns out that you need exactly 10 coins in your pocket: 3 quarters, 2 dimes, 1 nickel, and 4 pennies. With these in your arsenal, you can make perfect change for any bill.

How can you be sure? Brute force is your friend, as is PowerShell, of course.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
$coins = @{ 0.25 = 0; 0.10 = 0; 0.05 = 0; 0.01 = 0 }

function SelectCoins([Decimal] $change)
{
    $result = $coins.Clone()

    foreach($denomination in $coins.Keys | Sort -Desc)
    {
        while($change -ge $denomination)
        {
            $change -= $denomination
            $result[$denomination]++
        }
    }
   
    $result
}

$results = 1..99 | % { SelectCoins ($_ / 100) }

foreach($denomination in $coins.Keys | Sort -Desc)
{
    ("{0:c}: " -f $denomination) +
        ($results | % { $_[$denomination] } | 
            Measure-Object -Max).Maximum
}

 

Gives:

$0.25: 3
$0.10: 2
$0.05: 1
$0.01: 4

Comments [1] | | # 
 Tuesday, February 03, 2009
Tuesday, February 03, 2009 10:40:56 AM (Pacific Standard Time, UTC-08:00) ( )

Vladimir Averkin recently wrote a series of posts that show how to export code with syntax highlighting into HTML and RTF formats. It works great in Outlook, but was causing Windows Live Writer to crash. The reason is that the HTML stream of the clipboard isn’t just a blob of HTML – it’s supposed to be placed into the clipboard as CF_HTML. Investigation of that issue gave enough information to exactly pinpoint the crash in Live Writer, which they were quick to resolve once we pointed out. So it was a positive thing after all :)

While fixing the script, I took the opportunity to make the HTML prettier, work from both the ISE and the command-line, and fix a few bugs. I’ve posted it here: http://www.leeholmes.com/projects/scripts/Set-ClipboardScript.ps1.txt, as well as below.

 

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
################################################################################
# Set-ClipboardScript.ps1
#
# The script entire contents of the currently selected editor window to system
# clipboard. The copied data can be pasted into any application that supports
# pasting in UnicodeText, RTF or HTML format. Text pasted in RTF or HTML
# format will be colorized.
#
# See also:
# http://blogs.msdn.com/powershell/archive/2009/01/13/
# how-to-copy-colorized-script-from-powershell-ise.aspx
# http://www.leeholmes.com/blog/SyntaxHighlightingInPowerShell.aspx
# http://www.leeholmes.com/blog/RealtimeSyntaxHighlightingInYourPowerShellConsole.aspx
#
################################################################################

[CmdletBinding()]
param($path)

function Get-ScriptName
{
    $myInvocation.ScriptName
}

if($path -and ([Threading.Thread]::CurrentThread.ApartmentState -ne "STA"))
{
    PowerShell -NoProfile -STA -File (Get-ScriptName) $path
    return
}

$tokenColours = @{
    'Attribute' = '#FFADD8E6'
    'Command' = '#FF0000FF'
    'CommandArgument' = '#FF8A2BE2'
    'CommandParameter' = '#FF000080'
    'Comment' = '#FF006400'
    'GroupEnd' = '#FF000000'
    'GroupStart' = '#FF000000'
    'Keyword' = '#FF00008B'
    'LineContinuation' = '#FF000000'
    'LoopLabel' = '#FF00008B'
    'Member' = '#FF000000'
    'NewLine' = '#FF000000'
    'Number' = '#FF800080'
    'Operator' = '#FFA9A9A9'
    'Position' = '#FF000000'
    'StatementSeparator' = '#FF000000'
    'String' = '#FF8B0000'
    'Type' = '#FF008080'
    'Unknown' = '#FF000000'
    'Variable' = '#FFFF4500'
}

if($psise)
{
    $tokenColours = $psise.Options.TokenColors
}

Add-Type -Assembly System.Web
Add-Type -Assembly PresentationCore

# Create RTF block from text using named console colors.
function Append-RtfBlock ($block, $tokenColor)
{
    $colorIndex = $rtfColorMap.$tokenColor
    $block = $block.Replace('\','\\').Replace("`r`n","\cf1\par`r`n")
    $block = $block.Replace("`t",'\tab').Replace('{','\{').Replace('}','\}')
    $null = $rtfBuilder.Append("\cf$colorIndex $block")
}

# Generate an HTML span and append it to HTML string builder
$currentLine = 1
function Append-HtmlSpan ($block, $tokenColor)
{
    if (($tokenColor -eq 'NewLine') -or ($tokenColor -eq 'LineContinuation'))
    {
        if($tokenColor -eq 'LineContinuation')
        {
            $null = $codeBuilder.Append('`')
        }
       
        $null = $codeBuilder.Append("<br />`r`n")
        $null = $lineBuilder.Append("{0:000}<BR />" -f $currentLine)
        $SCRIPT:currentLine++
    }
    else
    {
        $block = [System.Web.HttpUtility]::HtmlEncode($block)
        if (-not $block.Trim())
        {
            $block = $block.Replace(' ', '&nbsp;')
        }

        $htmlColor = $tokenColours[$tokenColor].ToString().Replace('#FF', '#')

        if($tokenColor -eq 'String')
        {
            $lines = $block -split "`r`n"
            $block = ""

            $multipleLines = $false
            foreach($line in $lines)
            {
                if($multipleLines)
                {
                    $block += "<BR />`r`n"
                   
                    $null = $lineBuilder.Append("{0:000}<BR />" -f $currentLine)
                    $SCRIPT:currentLine++
                }

                $newText = $line.TrimStart()
                $newText = "&nbsp;" * ($line.Length - $newText.Length) + 
                    $newText
                $block += $newText
                $multipleLines = $true
            }
        }
   
        $null = $codeBuilder.Append(
            "<span style='color:$htmlColor'>$block</span>")
    }
}

function GetHtmlClipboardFormat($html)
{
    $header = @"
Version:1.0
StartHTML:0000000000
EndHTML:0000000000
StartFragment:0000000000
EndFragment:0000000000
StartSelection:0000000000
EndSelection:0000000000
SourceURL:file:///about:blank
<!DOCTYPE HTML PUBLIC `"-//W3C//DTD HTML 4.0 Transitional//EN`">
<HTML>
<HEAD>
<TITLE>HTML Clipboard</TITLE>
</HEAD>
<BODY>
<!--StartFragment-->
<DIV style='font-family:Consolas,Lucida Console; font-size:10pt;
    width:750; border:1px solid black; overflow:auto; padding:5px'>
<TABLE BORDER='0' cellpadding='5' cellspacing='0'>
<TBODY>
<TR>
    <TD VALIGN='Top'>
<DIV style='font-family:Consolas,Lucida Console; font-size:10pt;
    padding:5px; background:#cecece'>
__LINES__
</DIV>
    </TD>
    <TD VALIGN='Top' NOWRAP='NOWRAP'>
<DIV style='font-family:Consolas,Lucida Console; font-size:10pt;
    padding:5px; background:#fcfcfc'>
__HTML__
</DIV>
    </TD>
</TR>
</TBODY>
</TABLE>
</DIV>
<!--EndFragment-->
</BODY>
</HTML>
"@


    $header = $header.Replace("__LINES__", $lineBuilder.ToString())
    $startFragment = $header.IndexOf("<!--StartFragment-->") +
        "<!--StartFragment-->".Length + 2
    $endFragment = $header.IndexOf("<!--EndFragment-->") +
        $html.Length - "__HTML__".Length
    $startHtml = $header.IndexOf("<!DOCTYPE")
    $endHtml = $header.Length + $html.Length - "__HTML__".Length
    $header = $header -replace "StartHTML:0000000000",
        ("StartHTML:{0:0000000000}" -f $startHtml)
    $header = $header -replace "EndHTML:0000000000",
        ("EndHTML:{0:0000000000}" -f $endHtml)
    $header = $header -replace "StartFragment:0000000000",
        ("StartFragment:{0:0000000000}" -f $startFragment)
    $header = $header -replace "EndFragment:0000000000",
        ("EndFragment:{0:0000000000}" -f $endFragment)
    $header = $header -replace "StartSelection:0000000000",
        ("StartSelection:{0:0000000000}" -f $startFragment)
    $header = $header -replace "EndSelection:0000000000",
        ("EndSelection:{0:0000000000}" -f $endFragment)
    $header = $header.Replace("__HTML__", $html)
   
    Write-Verbose $header
    $header
}

function Main
{
    $text = $null
   
    if($path)
    {
        $text = (Get-Content $path) -join "`r`n"
    }
    else
    {
        if (-not $psise.CurrentOpenedFile)
        {
            Write-Error 'No script is available for copying.'
            return
        }
       
        $text = $psise.CurrentOpenedFile.Editor.Text
    }

    trap { break }

    # Do syntax parsing.
    $errors = $null
    $tokens = [system.management.automation.psparser]::Tokenize($Text,
        [ref] $errors)

    # Initialize HTML builder.
    $codeBuilder = new-object system.text.stringbuilder
    $lineBuilder = new-object system.text.stringbuilder
    $null = $lineBuilder.Append("{0:000}<BR />" -f $currentLine)
    $SCRIPT:currentLine++
  

    # Initialize RTF builder.
    $rtfBuilder = new-object system.text.stringbuilder
   
    # Append RTF header
    $header = "{\rtf1\fbidis\ansi\ansicpg1252\deff0\deflang1033{\fonttbl" +
        "{\f0\fnil\fcharset0 $fontName;}}"
    $null = $rtfBuilder.Append($header)
    $null = $rtfBuilder.Append("`r`n")

    # Append RTF color table which will contain all Powershell console colors.
    $null = $rtfBuilder.Append("{\colortbl ;")
   
    # Generate RTF color definitions for each token type.
    $rtfColorIndex = 1
    $rtfColors = @{}
    $rtfColorMap = @{}
   
    [Enum]::GetNames([System.Management.Automation.PSTokenType]) | % {
        $tokenColor = $tokenColours[$_];
        $rtfColor = "\red$($tokenColor.R)\green$($tokenColor.G)\blue" +
            "$($tokenColor.B);"
        if ($rtfColors.Keys -notcontains $rtfColor)
        {
            $rtfColors.$rtfColor = $rtfColorIndex
            $null = $rtfBuilder.Append($rtfColor)
            $rtfColorMap.$_ = $rtfColorIndex
            $rtfColorIndex ++
        }
        else
        {
            $rtfColorMap.$_ = $rtfColors.$rtfColor
        }
    }
   
    $null = $rtfBuilder.Append('}')
    $null = $rtfBuilder.Append("`r`n")
   
    # Append RTF document settings.
    $null = $rtfBuilder.Append('\viewkind4\uc1\pard\f0\fs20 ')
   
    # Iterate over the tokens and set the colors appropriately.
    $position = 0
    foreach ($token in $tokens)
    {
        if ($position -lt $token.Start)
        {
            $block = $text.Substring($position, ($token.Start - $position))
            $tokenColor = 'Unknown'
            Append-RtfBlock $block $tokenColor
            Append-HtmlSpan $block $tokenColor
        }
       
        $block = $text.Substring($token.Start, $token.Length)
        $tokenColor = $token.Type.ToString()
        Append-RtfBlock $block $tokenColor
        Append-HtmlSpan $block $tokenColor
       
        $position = $token.Start + $token.Length
    }

    # Append RTF ending brace.
    $null = $rtfBuilder.Append('}')
   
    # Copy console screen buffer contents to clipboard in three formats -
    # text, HTML and RTF.
    $dataObject = New-Object Windows.DataObject
    $dataObject.SetText([string]$text, [Windows.TextDataFormat]"UnicodeText")
    $rtf = $rtfBuilder.ToString()
    $dataObject.SetText([string]$rtf, [Windows.TextDataFormat]"Rtf")
    $code = $codeBuilder.ToString()
    $html = GetHtmlClipboardFormat($code)
   
    $dataObject.SetText([string]$html, [Windows.TextDataFormat]"Html")

    [Windows.Clipboard]::SetDataObject($dataObject, $true)
}

. Main
Comments [6] | | # 
 Monday, February 02, 2009
Monday, February 02, 2009 9:05:46 AM (Pacific Standard Time, UTC-08:00) ( )

We frequently get questions asking, “Where can I get a PowerShell script encoder so I can write secure scripts like the Visual Basic Script Encoder?”

The answer is that it is impossible to hide the password from the user if the script ever needs it. This is true of PowerShell, VBScript, C#, C++, Assembly, or any other language. There will always be some point when your script has reversed all of the encryption / protection mechanisms, giving the “attacker” complete access to it. If you don’t want the password itself hanging around in a script file, you can prompt the user for it. If the user is never supposed to know it, then you need to re-think your architecture.

Microsoft hasn’t been clear enough documenting what protections the Script Encoder offers, but here is an excerpt from the Scripting Guys:

Now, the important thing to keep in mind is that the script is simply encoded (or obfuscated); it is definitely not encrypted. What does that mean? That means the encoder will hide your script from most people; however, a truly determined hacker - armed with a knowledge of codes or armed with a utility downloaded from the Internet - could crack the code. Among other things, that means that you should never do something like “hide” an Administrator password in a script and assume that the Script Encoder will keep it safe from prying eyes. It won’t. It’s an encoder, not an encrypter, and there’s definitely a difference.

I’m not sure why the main download page is fond of the term “determined hacker” – a 30 second search for “vbe decryption” returns pages of results.

Now, a valid response to the whole situation is that you really only want to deter casual investigation, or that reversing the protection can then be linked to a breach of contract or software license. If you are in either of those boats, you don’t need an official tool to do this for you. Hiding your script behind Base64 encoding or ROT-13 should offer plenty of protection, and takes only a few lines of scripting. If you have the skill to make that decision, you have the skill to implement it as well.

Comments [5] | | # 
 Tuesday, January 20, 2009
Tuesday, January 20, 2009 5:39:22 PM (Pacific Standard Time, UTC-08:00) ( )

A while back, Roberto Farah published a script library to help control WinDbg through PowerShell. I’ve been using WinDbg for more debugging lately, and decided (after following one to many object references by hand) that I needed to script my investigations.

PowerDbg is definitely helpful – Roberto has tons of great scripts published that help analyze all kinds of interesting data. It also made me think of an alternative approach that works around some of the problem areas – PowerDbg uses SendKeys, window focusing, and used WinDbg logging as the communication mechanism. After some investigation, I thought that automation of the command-line version (cdb.exe) through its input and output streams might be easier and more efficient – which indeed it was.

You set up a remote from the Windbg instance you want to control, and then the WinDbg module (below) connects to the remote session (by manipulating standard in / standard out of cdb.exe) to manage its output.

Windows PowerShell V2
Copyright (C) 2008 Microsoft Corporation. All rights reserved.
PS C:\Users\leeholm> Import-Module Windbg
PS C:\Users\leeholm> Connect-Windbg "tcp:Port=10456,Server=SERVER"
PS C:\Users\leeholm> Invoke-WindbgCommand .symfix
PS C:\Users\leeholm> Invoke-WindbgCommand .reload
0:009> Reloading current modules
................................................................
...........
PS C:\Users\leeholm> Invoke-WindbgCommand k
0:009> ChildEBP RetAddr
0460fbe8 76ebd0d0 ntdll!DbgBreakPoint [d:\foo.c @ 206]
0460fc18 76fc4911 ntdll!DbgUiRemoteBreakin+0x3c [d:\bar.c @ 298]
0460fc24 76e6e4b6 kernel32!BaseThreadInitThunk+0xe [d:\baz.c @ 66]
0460fc64 76e6e489 ntdll!__RtlUserThreadStart+0x23 [d:\bing.c @ 2740]
0460fc7c 00000000 ntdll!_RtlUserThreadStart+0x1b [d:\bing.c @ 2672]
PS C:\Users\leeholm>

Here’s an example function to demonstrate its use:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
################################################################################
##
## Resolve-PsInvocationInfo.ps1
## Resolves the command and script information from an active PowerShell
## process
##
################################################################################
Import-Module windbg
Connect-Windbg "tcp:Port=10456,Server=SERVER"

function Resolve-Member($address, $member)
{
    $output = Invoke-WindbgCommand !dumpobj $address
   
    if(-not $member)
    {
        $output
    }
    else
    {
        $currentMember = ($member -split "\.")[0]
        $remaining = $member.Remove(0, $currentMember.Length).TrimStart('.')
       
        Write-Verbose "Looking for $currentMember on $address"
       
        $output = $output | 
            Select-String ("(instance|shared).*$currentMember" + '$') |
            Select-String -NotMatch 00000000

        if($output)
        {
            $columns = -split $output
            $address = $columns[6]
           
            Resolve-Member $address $remaining
        }
        else
        {
            Write-Error "Could not resolve $currentMember on $address"
        }
    }
}

$null = Invoke-WindbgCommand .loadby sos mscorwks
$output = Invoke-WindbgCommand ("!dumpheap -type " +
    "System.Management.Automation.InvocationInfo -short")
foreach($line in $output)
{
    Resolve-Member $line commandInfo.name
    Resolve-Member $line scriptToken._script
    Resolve-Member $line scriptToken._line
}

Disconnect-Windbg

And the module itself (WinDbg.psm1, place in a “WinDbg” folder in your modules folder):

(Edit: 05/13/09 - Updated to support local kernel debugging)

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
################################################################################
##
## WinDbg.psm1
## Automate WinDbg with PowerShell scripting
##
## To use:
## In WinDbg, set up a server: .server tcp:Port=10456
##
## In PowerShell
##
## Import-Module windbg
## Connect-Windbg -remote "tcp:Port=10456,Server=<server>"
## Other remote connection strings / protocols work, too.
##
## For local kernel debugging:
##
## Import-Module Windbg -ArgumentList 'C:\<path>\kd.exe'
## Connect-Windbg -ArgumentList "-kl"
## Invoke-WindbgCommand "!process"
##
## Cleaning up:
## Disconnect-Windbg
##
################################################################################

param($cdbPath = "C:\Program Files\Debugging Tools for Windows\cdb.exe")

$SCRIPT:windbgProcess = $null
$SCRIPT:currentConnection = $null

## Connect to a windbg remote session
function Connect-Windbg
{
    param(
        [Parameter(ParameterSetName = "Remote", Mandatory = $true)]
        $remote,

        [Parameter(ParameterSetName = "ArgumentList", Mandatory = $true)]
        $argumentList
        
        )
        
    if($SCRIPT:currentConnection -and ($SCRIPT:currentConnection -ne $remote))
    {
        throw "Already connected to $remote. Use Disconnect-Windbg, " + 
            "then connect to another instance."
    }
    
    ## Launch cdb.exe, the command-line version of WinDbg.
    ## Take control of its input and output streams, which we'll use
    ## to capture commands and their output.
    if(-not $SCRIPT:currentConnection)
    {
        $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo
        $processStartInfo.FileName = $cdbPath
        $processStartInfo.WorkingDirectory = (Get-Location).Path

        if($remote)
        {
                $processStartInfo.Arguments = "-remote $remote"
        }
        else
        {
                $processStartInfo.Arguments = $argumentList
        }
        
        $processStartInfo.UseShellExecute = $false
        $processStartInfo.RedirectStandardInput = $true
        $processStartInfo.RedirectStandardOutput = $true 

        $SCRIPT:windbgProcess = 
            [System.Diagnostics.Process]::Start($processStartInfo)
        $SCRIPT:currentConnection = $remote

        if(-not $SCRIPT:windbgProcess)
        {
            return
        }
        
        ## Ignore the stuff that was in the session before we
        ## connected
        $null = Receive-WindbgOutput
    }
}

## Disconnect from the session
function Disconnect-Windbg
{
    if($SCRIPT:windbgProcess -and (-not $SCRIPT:windbgProcess.HasExited))
    {
        $SCRIPT:windbgProcess.StandardOutput.Close()
        $SCRIPT:windbgProcess.StandardInput.Close()
        $SCRIPT:windbgProcess.Kill()
    }
    
    $SCRIPT:currentConnection = $null
    $SCRIPT:windbgProcess = $null
}

## Invoke a command in the connected WinDbg session, and return
## its output
function Invoke-WindbgCommand
{
    if(-not $SCRIPT:windbgProcess)
    {
        throw "Not connected. Use Connect-Windbg to connect to an " +
            "instance of WinDbg."
    }
    
    $SCRIPT:windbgProcess.StandardInput.WriteLine("$args")
    Receive-WindbgOutput
}

## Retrieve pending output from the connected WinDbg session
function Receive-WindbgOutput
{
    ## Add a special tag so that we know the end of the command
    ## response
    $sent = "PSWINDBG_COMPLETE_{0:o}" -f [DateTime]::Now        
    $SCRIPT:windbgProcess.StandardInput.WriteLine(".echo $sent")
    
    $received = New-Object System.Text.StringBuilder

    ## Wait for the response to end
    while($received.ToString().IndexOf($sent) -lt 0)
    {
        $null = $SCRIPT:windbgProcess.StandardOutput.EndOfStream
        while($SCRIPT:windbgProcess.StandardOutput.Peek() -ge 0)
        {
            $null = $received.Append(
                $SCRIPT:windbgProcess.StandardOutput.Read(), 1)
        }
    }
    
    ## Split it into lines, and return everything but the new
    ## prompt, and the "end response" tag
    $output = $received.ToString().Split("`r`n")
    if($output.Length -gt 2)
    {
        $output[0..($output.Length - 3)]
    }
}

Export-ModuleMember -Function Connect-Windbg
Export-ModuleMember -Function Disconnect-Windbg
Export-ModuleMember -Function Invoke-WindbgCommand

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Disconnect-Windbg }
Comments [5] | | # 
 Sunday, January 18, 2009
Sunday, January 18, 2009 4:43:25 PM (Pacific Standard Time, UTC-08:00) ( )

In version 1 of PowerShell, it was possible to access Win32 APIs in one of two ways: by generating a dynamic assembly on the fly (you wouldn’t really do this for one-off calls, but would probably do it in a script that makes it easier to invoke Win32 APIs,) or by looking up the P/Invoke definition for that API call, and compiling in-line C# to access it.

These are both good approaches, but we wanted to use the Add-Type cmdlet to really nail the language interop scenarios for PowerShell V2. Add-Type offers four basic modes of operation:

PS:13 > Get-Command Add-Type | Select -Expand ParameterSets | Select Name

Name
----
FromSource
FromMember
FromPath
FromAssemblyName

These are:

  • FromSource: Compile some C# (or other language) code that completely defines a type. This is useful when you want to define an entire class, its methods, namespace, etc. You supply the actual code as the value to the –TypeDefinition parameter, usually through a variable.
  • FromPath: Compile from a file on disk, or load the types from an assembly at that location.
  • FromAssemblyName: Load an assembly from the GAC by its shorter name. This is is not the same as [Reflection.Assembly]::LoadWithPartialName, since that introduces your script to many subtle breaking changes. Instead, PowerShell maintains a large mapping table that converts the shorter name you type a strongly-named assembly reference.
  • FromMember: Generates a type out of a member definition (or set of them.) For example, if you specify only a method definition, PowerShell automatically generates the wrapper class for you. This parameter set is explicitly designed to easily support P/Invoke calls.

Now, how do you use the FromMember parameter set to call a Win32 API? Let’s see. First, imagine that you want to access sections of an INI file. This previous blog post gives an example for V1.

PowerShell doesn’t have a native way to manage INI files, and neither does the .NET Framework. However, the Win32 API does, through a call to GetPrivateProfileString. The .NET framework lets you access Win32 functions through a technique called P/Invoke (Platform Invocation Services.) Most calls boil down to a simple “P/Invoke definition,” which usually takes a lot of trial and error. However, a great community has grown around these definitions, resulting in an enormous resource called P/Invoke .NET. The P/Invoke Interop Assistant is an awesome tool from the CLR team that also generates these definitions.

First, we’ll create a script, Get-PrivateProfileString.ps1. It’s a template for now:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$null

So first, we visit P/Invoke .NET and search for GetPrivateProfileString:

image

Click into the definition, and we see the C# signature:

image

Copy that signature as a here-string into our script. Notice that we’ve added public to the declaration. The signatures on PInvoke.NET assume that you’ll call the method from within the C# class that defines it. We’ll be calling it from scripts, so we need to change its visibility.

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@


$null

Now, we add the call to Add-Type. This signature becomes the building block for a new class, so we only need to give it a name. To prevent its name from colliding with other classes with the same name, we also put it in a namespace. The name of our script is a good choice:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@


$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -PassThru

$null

When we try to run this script, though, we get an error:

The type or namespace name 'StringBuilder' could not be found (are you missing a
using directive or an assembly reference?)
c:\Temp\obozeqo1.0.cs(12) :    string lpDefault,
c:\Temp\obozeqo1.0.cs(13) : >>>    StringBuilder lpReturnedString,
c:\Temp\obozeqo1.0.cs(14) :    uint nSize,

Indeed we are. StringBuilder is defined in the System.Text namespace, but the using directive goes at the top of the program by the class definition. Since we’re letting PowerShell define the type for us, we can either rename it to System.Text.StringBuilder, or add a –UsingNamespace parameter. (Aside: PowerShell adds references to the System and System.Runtime.InteropServices namespaces by default.) Let’s do the latter:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@


$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -Using System.Text -PassThru

$null

Now, we can plug in all of the necessary parameters. The GetPrivateProfileString puts its output in a StringBuilder, so we’ll have to feed it one, and return its contents:

## Get-PrivateProfileString.ps1
param(
    $file,
    $category,
    $key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@


$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -Using System.Text -PassThru
   
$builder = New-Object System.Text.StringBuilder 1024
$type::GetPrivateProfileString($category,
    $key, "", $builder, $builder.Capacity, $file)
   
$builder.ToString()

So now we have it. With just a few lines of code, we’ve defined and invoked a Win32 API call.

[C:\Users\leeholm]
PS:1 > Get-PrivateProfileString c:\windows\system32\tcpmon.ini "<Generic Network Card>" Name
Generic Network Card

Comments [2] | | # 
 Tuesday, January 13, 2009
Tuesday, January 13, 2009 8:36:11 AM (Pacific Standard Time, UTC-08:00) ( )

One thing that sometimes comes as a surprise (most recently as a comment on the PowerShell blog) is that DateTime casts are always parsed in the en-US DateTime format:

Casting to a DateTime object doesn't respect the current culture of the thread. For example:

> [Threading.Thread]::CurrentThread.CurrentCulture

LCID  Name

----  ----

2057  en-GB

> [DateTime] "01/02/03"

02 January 2003

> # Note US date interpretation.

This is an explicit design goal.

To prevent subtle internationalization issues from popping into your scripts, PowerShell treats "[DateTime] '11/26/2007'" (a date constant) like a language feature – just as it does [Double] 10.5 (a numeric constant.) Not all cultures use the decimal point as the fractions separator, but programming languages standardize on it. Not all cultures use the en-US DateTime format, resulting in millions of internationalization bugs when people don’t consider the impact of having their software run in those cultures.

Imagine a random script that you find on the internet. It was probably written on a system that uses a different culture than you or I. For the script to run correctly our machines, the author needs to write in a culturally-sensitive way:

1) The approach taken by traditional software development for internationalization:

$date = New-Object System.DateTime 2007,11,27,17,56,00

or (an imagined PowerShell syntax)

$date = [DateTime->pt-PT] "terça-feira, 27 de Novembro de 2007 9:34:50"

2) An approach that assumes a specific culture, which is the approach PowerShell takes:

$date = [DateTime] "11/27/2007 5:56:00 pm"

Approach #2 is much more user-friendly, as it minimizes what you need to remember. In approach #1, you always need to remember to specify your locale when you create the DateTime. In approach #2, you always need to remember to use the en-US format (as you already do when writing floating-point constants.)

The additional benefit of assuming a specific culture is that you are protected from people that do NOT write the script in a culturally-sensitive way. While some advanced scripters might be able to understand approach #1, most scripters (and even professional software developers) do not. History has shown us that software not explicitly TESTED for internationalization fails spectacularly. I don't mean "written for international markets," I mean TESTED for them.

When an admin bashes together a script because their hair is on fire, and then wants to share it later -- testing for other regions is not high on the list of things to do.

Comments [4] | | # 
 Friday, January 09, 2009
Friday, January 09, 2009 11:17:11 PM (Pacific Standard Time, UTC-08:00) ( )

When it comes to personal finance, research and reading sometimes can’t answer all of your questions. Especially when it comes to projecting the future, many subtle factors are simply too difficult to easily describe in rules of guidance. Because of this, they tend to be ignored.

With interest rates sinking, now’s a good time to evaluate a mortgage refinance. The online calculators help you figure out if it’s right for you, but they all miss important facts.

Let’s pretend that you refinance 5 years into your current 30-year mortgage (that started at 108k), plan to move in 5 years, and your house value doesn’t change. You’ve now got a 100k mortgage at 6%, and want to re-finance at 5%.

The immediate benefit pointed out is that your monthly payment will drop, and that the new interest-heavy payment will be even more tax deductible.

At 6%, you’re paying $647 per month.
At 5%, you’re paying $585 per month.

However,

  • All of the re-finance quotes and calculators put your remaining balance into a new 30-year mortgage. That pushes the quoted monthly figures even lower than $585 – to be expected if you spread 25 years of payments over 30 years! If you want to take the approach of not building principal to reduce monthly payments, you might as well go for an interest-only loan, or rent.
  • Even if you put yourself into a 25-year mortgage instead, loan amortization over the first 5 years of that new loan may be much more interest-heavy than the mid years of your current loan. That means a reduction in your principal payments, which is a direct loss of money when you sell your house in 5 years. In this specific scenario (you are only 5 years into a mortgage,) you’re actually building principal faster in the new loan.
    • Your current loan will build $10,117 in principal in this time
    • A new loan will build $11,252 in principal in this time

As we can see from these numbers, this is not a negative impact of the scenario I laid out above. However, it will impact more mature loans.

  • Re-financing means paying the closing costs all-over again. If you assume 10k out-of-pocket, you can instead apply that as a balloon payment and drop your monthly payment to $579 per month – lower than the rate you would get from a re-finance.
  • If you don’t want to pay the closing costs out of pocket and instead roll them into your mortgage, that gives you a $110k loan @ $643 per month.

These calculations are obviously sensitive to real numbers, but they describe core decision points that are not covered in most literature.

Since we have a powerful scripting language at our disposal, however, we are in a position to let a computer model help guide our decisions.

We’ve done this before (Traditional or Roth 401(k)?,) so let’s do it again.

## Assumptions:
##     You will invest the closing fee if you don't need to spend it
##     You will bank any improvement in monthly fees

$currentLoan = 100000
$currentRate = 6.125 / 100
$newRate  = 4.25 / 100
$origination = 1.00 / 100
$closing = 5000
$monthsTotal = 25 * 12

## Scenarios:
$scenarios = @(
    ## 0: Current loan. Invest closing fee instead @ 3%
    @{ LoanAmount = $currentLoan; Interest = $currentRate;
       Saved = $closing * [Math]::Pow(1.03, 10) },
    
    ## 1: Put closing fee against principal on current loan
    @{ LoanAmount = $currentLoan - $closing; Interest = $currentRate;
       Saved = 0 },
    
    ## 2: Refi: Add origination fee onto mortage, and spend
    ## closing fee to refinance
    @{ LoanAmount = $currentLoan + ($currentLoan * $origination);
       Interest = $newRate; Saved = 0 },
    
    ## 3: Refi: Add origination and closing fees onto mortage,
    ## and invest closing fee
    @{ LoanAmount = $currentLoan +
           ($currentLoan * $origination) + $closing;
       Interest = $newRate; Saved = $closing * [Math]::Pow(1.03, 10) }
)

## Calculate the monthly payments
$maxPayment = 0
$counter = 0
foreach($scenario in $scenarios)
{
    $scenario.InterestPerPeriod = $scenario.Interest / 12

    $scenario.MonthlyPayment = $scenario.LoanAmount * 
        $scenario.InterestPerPeriod *
        [Math]::Pow(1 + $scenario.InterestPerPeriod, $monthsTotal) /
        ([Math]::Pow(1 + $scenario.InterestPerPeriod, $monthsTotal) - 1)

    "Monthly payment $counter is: " + $scenario.MonthlyPayment
    if($scenario.MonthlyPayment -gt $maxPayment)
    {
        $maxPayment = $scenario.MonthlyPayment
    }
    
    $scenario.Balance = $scenario.LoanAmount
    $counter++
}

## Apply the monthly payments. This applies standard
## amortization, which pays off the interest that built up
## that month before applying payments to principal.
$results = @()
for($month = 1; $month -le $monthsTotal; $month++)
{
    $outputObject = New-Object PsObject
    Add-Member -in $outputObject NoteProperty Month $month

    ## Calculate the principal, interest, and total savings
    ## for each scenario
    $counter = 0
    foreach($scenario in $scenarios)
    {
        $scenario.Balance *= (1 + $scenario.InterestPerPeriod)
        $interestPayment = $scenario.InterestPerPeriod * $scenario.Balance
        $principal = $scenario.MonthlyPayment - $interestPayment
        
        # Put the following into the bank / savings:
        # the principal payment, the difference between this payment
        # and the most expensive, and the tax-writeoff aspect of the
        # interest
        $scenario.Saved += $principal
        $scenario.Saved += $maxPayment - $scenario.MonthlyPayment
        $scenario.Saved += $interestPayment * 0.3
        
        Add-Member -in $outputObject `
            NoteProperty Interest_$counter ("{0:C}" -f $interestPayment)
        Add-Member -in $outputObject `
            NoteProperty Principal_$counter ("{0:C}" -f $principal)
        Add-Member -in $outputObject `
            NoteProperty Balance_$counter ("{0:C}" -f $scenario.Balance)
        Add-Member -in $outputObject `
            NoteProperty Saved_$counter ("{0:C}" -f $scenario.Saved)
        
        
        $scenario.Balance -= $scenario.MonthlyPayment
        $counter++
    }
    
    $results += $outputObject
}

$results| Format-Table Month,S*,B*
#$results| Format-Table Month,S*,I*,P*,B*

This gives you a table of information that lets you easily determine which scenario is best for you, and how long the refinance will take to pay for itself:

________________________________________________________________________________________________________________________
PS E:\Lee> E:\Lee\RefinanceCalculator_Example.ps1
Monthly payment 0 is: 651.963964563932
Monthly payment 1 is: 619.365766335736
Monthly payment 2 is: 547.155481990353
Monthly payment 3 is: 574.24238703938

        Month Saved_0       Saved_1       Saved_2      Saved_3      Balance_0    Balance_1    Balance_2    Balance_3  
        ----- -------       -------       -------      -------      ---------    ---------    ---------    ---------  
            1 $7,012.43     $310.80       $400.68      $7,107.82    $100,510.42  $95,484.90   $101,357.71  $106,375.42
            2 $7,305.79     $622.09       $801.83      $7,496.56    $100,368.15  $95,349.74   $101,167.59  $106,175.89
            3 $7,599.66     $933.86       $1,203.46    $7,885.79    $100,225.15  $95,213.89   $100,976.80  $105,975.65
            4 $7,894.04     $1,246.12     $1,605.56    $8,275.52    $100,081.43  $95,077.35   $100,785.33  $105,774.70

If it doesn’t cover a scenario you’re wondering about, well that’s OK too! That’s the beauty of scripts – you can change them.

Comments [0] | | #