A pedant that hangs out in the dark corner-cases of the web.

Wednesday, October 30, 2013

PowerShell script to parse ASP.NET errors from the event log

Here is a script that exhaustively parses out the ASP.NET details from event log entries.


Parses ASP.NET errors from the event log on the given server.
.Parameter ComputerName
The name of the server on which the error occurred.
.Parameter EntryType
Gets only events with the specified entry type. Valid values are Error, Information, and Warning. The default is all events.
.Parameter After
Skip events older than this datetime.
.Parameter Before
Skip events newer than this datetime.
.Parameter Newest
The maximum number of the most recent events to return.

#requires -version 3
[CmdletBinding()] Param(
$FieldNames= @(
) |sort Length
$RemoveFields= '_','ThreadAccountName','ReqThreadAccountName' # blank or redundant fields
$BoolFields= 'IsAuthenticated','IsImpersonating'
$IntFields= 'EventOccurrence','EventSequence','EventCode','EventDetailCode','ProcessId','ThreadId'
$EventQuery = @{
    ComputerName = $ComputerName
    LogName      = 'Application'
    Source       = 'ASP.NET 4.0.30319.0','ASP.NET 2.0.50727.0','ASP.NET 1.1.4322.0'
Get-EventLog @EventQuery |
    ? {1017,1019,1023,1025 -notcontains $_.EventID} | # don't want ASP.NET registration events
    % {
        [string]$type = $_.EntryType
        $fields = @{EntryType=$type;Source=$_.Source;EventTime=$_.TimeGenerated}
        if($type -eq 'Error')
        { # errors aren't structured nicely
            if($_.Message -match '(?m)^Application ID: (?.+)$'){$fields.AppId=$Matches.AppId.TrimEnd()}
            if($_.Message -match '(?m)^Process ID: (?.+)$'){$fields.ProcessId=[int]$Matches.ProcessId.TrimEnd()}
            if($_.Message -match '(?m)^Exception: (\w+\.)*(?\w+)\s*$'){$fields.ExceptionType=$Matches.ExceptionType}
            if($_.Message -match '(?m)^Message: (?.+)$'){$fields.ExceptionMessage=$Matches.ExceptionMessage.TrimEnd()}
            if($_.Message -match '(?ms)^StackTrace: (?.+)$'){$fields.StackTrace=$Matches.StackTrace.TrimEnd()}
            $values = $_.ReplacementStrings
            $names = $FieldNames |? Length -ge $values.Length |select -f 1
            if($values.Length -gt $names.Length) { Write-Warning ('Unexpected field values: {0} > {1}' -f $values.Length,$names.Length) }
            for($i=0; $i -lt $values.Length; $i++) {$fields[$names[$i]]= $values[$i].TrimEnd()}
            $RemoveFields |% {$fields.Remove($_)}
            $BoolFields |% {$fields[$_]=[bool]$fields[$_]}
            $IntFields |% {$fields[$_]=[int]$fields[$_]}
            $fields.RequestUrl= [uri]$fields.RequestUrl
            $fields.EventTime= [datetime]::Parse($fields.EventTime,$null,[Globalization.DateTimeStyles]::AssumeLocal)
            $fields.EventTimeUtc= [datetime]::Parse($fields.EventTimeUtc,$null,[Globalization.DateTimeStyles]::AssumeUniversal)
            if($fields.ExceptionMessage -and $fields.StackTrace)
            { $fields.ExceptionMessage= $fields.ExceptionMessage.Replace($fields.StackTrace,'').TrimEnd() } # don't need stack trace twice
        $event = New-Object PSObject -p $fields

Tuesday, May 07, 2013


Searches files for pattern, displays matches, opens in text editor.
.Parameter Pattern
Specifies the text to find. Type a string or regular expression. 
If you type a string, use the SimpleMatch parameter.
.Parameter Filters
Specifies wildcard filters that files must match one of.
.Parameter Path
Specifies a path to one or more locations. Wildcards are permitted. 
The default location is the current directory (.).
.Parameter Include
Wildcard patterns files must match one of (slower than Filter).
.Parameter Exclude
Wildcard patterns files must not match any of.
.Parameter CaseSensitive
Makes matches case-sensitive. By default, matches are not case-sensitive. 
.Parameter List
Returns only the first match in each input file. 
By default, Select-String returns a MatchInfo object for each match it finds.
.Parameter NotMatch
Finds text that does not match the specified pattern.
.Parameter SimpleMatch
Uses a simple match rather than a regular expression match. 
In a simple match, Select-String searches the input for the text in the Pattern parameter. 
It does not interpret the value of the Pattern parameter as a regular expression statement.
.Parameter NoRecurse
Disables searching subdirectories.
C:\PS> Find-Lines 'using System;' *.cs "$env:USERPROFILE\Documents\Visual Studio*\Projects" -CaseSensitive -List

This command searches all of the .cs files in the Projects directory (or directories) and subdirectories,
displays the first matching line of each file with a match, then opens the file to the correct line in the editor.
# set up splatting
$lsopt = @{Recurse=!$NoRecurse}
if($Path) { $lsopt.Path=$Path }
if($Include) { $lsopt.Include=$Include }
if($Exclude) { $lsopt.Exclude=$Exclude }
$ssopt = @{'Pattern'=$Pattern}
if($CaseSensitive) { $ssopt.CaseSensitive=$true }
if($List) { $ssopt.List=$true }
if($NotMatch) { $ssopt.NotMatch=$true }
if($SimpleMatch) { $ssopt.SimpleMatch=$true }
# the filter parameter is much faster than the include parameter
Select-String -Path ($( if($Filters) { $Filters|% {ls @lsopt -Filter $_} } else { ls @lsopt } ) |
    ? {Test-Path $_.FullName -PathType Leaf}) @ssopt |
  ogv -p -t "Search: '$Pattern' $Filters" |
  % {emeditor $_.Path /l $_.LineNumber} #TODO: customize editor

Thursday, April 11, 2013

TLS (HTTPS) query strings: encrypted

Wednesday, April 10, 2013


A simple PowerShell script to copy scheduled tasks from one machine (often much older) to another.
Copy scheduled jobs from another computer to this one, using a GUI list to choose jobs.
.Parameter ComputerName
The name of the computer to copy jobs from.
.Parameter DestinationComputerName
The name of the computer to copy jobs to (local computer by default).
[Parameter(Position=1)][Alias('To','Destination')][string]$DestinationComputerName = $env:COMPUTERNAME
$TempXml= [io.path]::GetTempFileName()
$CredentialCache = @{}
function Get-CachedCredentials([Parameter(Mandatory=$true,Position=0)][string]$UserName)
    { $CredentialCache.Add($UserName,(Get-Credential -Message "Enter credentials for $UserName tasks" -UserName $UserName)) }
function ConvertFrom-Credential([Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)]$Credential)
{ $Credential.GetNetworkCredential().Password }
schtasks /query /s $ComputerName /v /fo csv |
    ConvertFrom-Csv |
    ogv -p -t 'Select jobs to copy' |
    select TaskName,'Run As User' -Unique |
    % {
        schtasks /query /s $ComputerName /tn $_.TaskName /xml ONE |Out-File -Encoding unicode $TempXml
        schtasks /create /s $DestinationComputerName /tn $_.TaskName /ru ($_.'Run As User') `
            /rp (Get-CachedCredentials $_.'Run As User' |
            ConvertFrom-Credential) /xml $TempXml
        rm $TempXml

Wednesday, January 23, 2013

Installing to the GAC

In earlier versions of the .NET Framework, the Shfusion.dll Windows shell extension enabled you to install assemblies by dragging them in File Explorer. Beginning with the .NET Framework 4, Shfusion.dll is obsolete.

Gacutil.exe is only for development purposes and should not be used to install production assemblies into the global assembly cache.

(Also: Yikes, there are multiple GACs (by CLR, and by 32/64-bit processor architecture): .NET 4.0 has a new GAC, why? - Stack Overflow)

Replacement for the alternative: http://wixtoolset.org/ (not http://www.wix.com/) ("Standard Custom Actions"?):