All posts by Freakling

Beta testing and error handling

Here is a simple snippet that you can run to output all exceptions in a script.

$ErrorActionPreference = "SilentlyContinue"

IWillProduceAnError

#----------------------------------------------------------------
# Debug function, dumps all errors in $Error variable to file
#----------------------------------------------------------------

Function Dump-Errors(){
    Param(
        [Parameter(Position=0, Mandatory = $True)]
        [String]$Logfile
    )
    $Error | Add-Content $Logfile
    $Error.Clear()
}

Dump-Errors -logfile "c:\temp\script.log"

If you look at the code, the cmdlet “IWillProduceAnError” is most likely not recognized and will produce an exception.
However, you have the $ErrorActionPreference set to SilentlyContinue so nothing will show for the user.

This is where the dump-errors function will help you catch errors you would otherwise never see.

Place the function in your code and make sure the last line to run is the function.
It will output all exceptions in the $Error variable to a file. You can then later collect the file from the users you know are beta testing and use the Try/Catch statements to correct your code. Combine this with the Powershell auto updater in silent mode and the user will never know you updated the code.

Office 2007 and .ost files.

There is a slight issue with .ost files and roaming profiles in Outlook 2007.
The issue appears when the user gets a corrupt profile, gets another generated but still has some registry keys mapped to the old corrupt profile. If the corrupt profile gets removed, or if another local mail account is created on the new profile it will result in an error when the user switches between computers.

Here is the logonscript that solves the issue by changing the keys each time the user logs on.

On Error Resume Next

If Not TestOstFilePath = True Then
  Const HKEY_CURRENT_USER = &H80000001
  Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
  Set objFSO = CreateObject("Scripting.FileSystemObject")
  Set oShell = CreateObject("WScript.Shell")
    
  LocalAppData = oShell.ExpandEnvironmentStrings("%LocalAppData%")
  stringDefaultOstPath = LocalAppdata & "\Microsoft\Outlook\"
  
  ' searches for the most recent created .ost file.
  Set objFSO = CreateObject("scripting.filesystemobject")
  set oFolder = objFSO.getfolder(stringDefaultOstPath)
  For Each aFile In oFolder.Files
    Extension = objFSO.GetExtensionName(stringDefaultOstPath & aFile)
    If Extension = "ost" Then
      If sNewest = "" Then
        Set fNewest = aFile
        sNewest = aFile.Name
      Else
        If fNewest.DateCreated < aFile.DateCreated Then
          Set fNewest = aFile
        End If
      End If
    End If
  Next
  
  ' if no .ost file is found, revert to default.
  If fNewest = "" Then
    fNewest = stringDefaultOstPath & "outlook.ost"
  End If
  
  ' Convert path into binary format with nulls
  stringDefaultOstFilePath = fNewest
  
  iLength = Len(stringDefaultOstFilePath)
  Dim binaryDefaultOstFilePath()
  Redim binaryDefaultOstFilePath(iLength * 2 + 1)
  For i = 0 To iLength - 1
    binaryDefaultOstFilePath(i * 2)   = Asc(Mid(stringDefaultOstFilePath, i + 1, 1))
    binaryDefaultOstFilePath(i * 2 + 1) = 0
  Next
  binaryDefaultOstFilePath(iLength * 2)   = 0
  binaryDefaultOstFilePath(iLength * 2 + 1) = 0'
  
  ' Look in outlook profile and set the ost to correct location
  iReturn = oReg.EnumKey(HKEY_CURRENT_USER, "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\", allOutlookProfiles)
  If IsArray(allOutlookProfiles) Then
    For Each stringOutlookProfile In allOutlookProfiles
      iReturn = oReg.SetBinaryValue(HKEY_CURRENT_USER, "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\" & stringOutlookProfile & "\13dbb0c8aa05101a9bb000aa002fc45a", "001f6610", binaryDefaultOstFilePath)
      If iReturn <> 0 Then
        oShell.Logevent 1, "Error! Could not set value 001f6610 to " & binaryDefaultOstFilePath & ". Error: " & iReturn
      End If
      iReturn = oReg.SetStringValue(HKEY_CURRENT_USER, "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\" & stringOutlookProfile & "\13dbb0c8aa05101a9bb000aa002fc45a", "001e660e", stringDefaultOstPath)
      If iReturn <> 0 Then
        oShell.Logevent 1, "Error! Could not set value 001e660e to " & stringDefaultOstPath & ". Error: " & iReturn
      End If
    Next
  End If
  
  ' Writes an event with the current .ost file to registry.
  RegistrySetting = oShell.RegRead("HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Microsoft Outlook\13dbb0c8aa05101a9bb000aa002fc45a\001f6610")
  If Not RegistrySetting = "" Then
    oShell.Logevent 4, "The .ost path has been set to: " & stringDefaultOstFilePath
  Else 
    oShell.Logevent 2, "The .ost path can not be found"
  End If
End If

Function TestOstFilePath
  On Error Resume Next
  Const HKEY_CURRENT_USER = &H80000001
  Set objFSO = CreateObject("Scripting.FileSystemObject")
  Set oShell = CreateObject("WScript.Shell")
  
  strKeyPath = "HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Microsoft Outlook\13dbb0c8aa05101a9bb000aa002fc45a\"
  strValueName = "HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Microsoft Outlook\13dbb0c8aa05101a9bb000aa002fc45a\001f6610"
  
  RegExists = True
  RegistrySetting = oShell.RegRead(strValueName)
  If Err.Number <> 0 Then
    oShell.Logevent 2, "Could not read from registry. " & Err.Description
    RegExists = False
    Err.Clear
  End If
  
  StringRegValue=""  
  for i=0 to ubound(RegistrySetting)
    if RegistrySetting(i)<>0 then StringRegValue=StringRegValue & chr(RegistrySetting(i))
  Next

  If RegExists = True Then
    If objFSO.FileExists(StringRegValue) Then
      TestOstFilePath = True
    Else
      TestOstFilePath = False
    End If
  Else
    TestOstFilePath = True
  End If
End Function

I did not come up with everything myself. Some of the code was taken from the site below. But i felt that this did not solve my issue completely since i do not use “ForceOSTPath” value.

Some functionality was taken from:
http://www.greycube.com/help/vb/update_ost_path_in_outlook_profiles.txt

Windows forms, tips and trix – Buttons

Ever written a GUI to your application? Ever think there are alot of unnecessary lines?
Here is a smart way to add basic buttons without too many lines.

function CreateButton{
  param(
    $name,
    $text,
    $size,
    $location,
    $onclick,
    $Form
  )
  $name = New-Object System.Windows.Forms.Button
  $name.Text = "$text"
  $name.Location = "$location"
  $name.Size = "$size"
  $name.add_Click($onclick)
  $Form.Controls.Add($name)
}

This requires a specific type of code to be run on click. We declare a variable as code within curlybrackets as shown below. In this case we use System.Windows.Forms.SaveFileDialog and save a richtextbox named $consolewindow.

$SaveButton_Click = {
  $SaveFileDialog = New-Object System.Windows.Forms.SaveFileDialog
  $SaveFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
  $SaveFileDialog.ShowDialog()
  $ConsoleWindow.Text | Out-File $SaveFileDialog.FileName
  $SaveFileDialog.Dispose()
}

To add a button with this information we need to create a main form and the console window with the code below.

[Void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

# Main form settings
$Form = New-Object System.Windows.Forms.Form
$Form.ClientSize = "1000,620"
$Form.DesktopLocation = "100,100"
$Form.MaximizeBox = $False
$Form.ShowIcon = $False
$Form.FormBorderStyle = "FixedSingle"
$Form.Name = "Example"
$Form.Text = "Example"

# Console window settings
$ConsoleWindow = New-Object System.Windows.Forms.RichTextBox
$ConsoleWindow.Size = "1000,250"
$ConsoleWindow.Location = "0,320"
#$ConsoleWindow.ReadOnly = $True #commented out for manual input.
$ConsoleWindow.BackColor = "Black"
$ConsoleWindow.ForeColor = "White"
$Form.Controls.Add($Consolewindow)

I’ve added a few extra rows just to make it look good for the example.
Now let’s create a button

CreateButton -name "SaveButton" -text "Save to file" -size "100,30" -location "50,50" -form $form -onclick $SaveButton_Click

Then we load the forms window

$Form.ShowDialog()

 

Normally you might not have a use for this function but when creating dynamic buttons you could have a use for it. To create a range of buttons from an array, use something like this.

$x = 0
$y = 0
for($i=0; $i -lt 20;$i++){
  CreateButton -name $Buttons[$i] -text $Buttons[$i] -size "100,30" -location "$x,$y" -form $form -onclick $Button_Click[$i]
  $x += 100
  if($x -gt "900"){$y += 30;$x = 0}
}

This example takes the first 20 from an array and puts them on the main form in a orderly fashion as to not overcrowd the main window. The example has no practical use, but you could extract nestled objects and create dynamic buttons with a bit of work.

The full script would look like this:
Note that i’m using the same function for all buttons.

function CreateButton{
  param(
    $name,
    $text,
    $size,
    $location,
    $onclick,
    $Form
  )
  $name = New-Object System.Windows.Forms.Button
  $name.Text = "$text"
  $name.Location = "$location"
  $name.Size = "$size"
  $name.add_Click($onclick)
  $Form.Controls.Add($name)
}
$SaveButton_Click = {
  $SaveFileDialog = New-Object System.Windows.Forms.SaveFileDialog
  $SaveFileDialog.Filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
  $SaveFileDialog.ShowDialog()
  $ConsoleWindow.Text | Out-File $SaveFileDialog.FileName
  $SaveFileDialog.Dispose()
}
[Void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

# Main form settings
$Form = New-Object System.Windows.Forms.Form
$Form.ClientSize = "1000,620"
$Form.DesktopLocation = "100,100"
$Form.MaximizeBox = $False
$Form.ShowIcon = $False
$Form.FormBorderStyle = "FixedSingle"
$Form.Name = "Example"
$Form.Text = "Example"

# Console window settings
$ConsoleWindow = New-Object System.Windows.Forms.RichTextBox
$ConsoleWindow.Size = "1000,250"
$ConsoleWindow.Location = "0,320"
#$ConsoleWindow.ReadOnly = $True #commented out for manual input.
$ConsoleWindow.BackColor = "Black"
$ConsoleWindow.ForeColor = "White"
$Form.Controls.Add($Consolewindow)

$Buttons = @("1".."26")
$x = 0
$y = 0
for($i=0; $i -lt 20;$i++){
  if($buttons[$i] -eq $null){break}
  CreateButton -name $Buttons[$i] -text $Buttons[$i] -size "100,30" -location "$x,$y" -form $form -onclick $SaveButton_Click
  $x += 100
  if($x -gt "900"){$y += 30;$x = 0}
}

$Form.ShowDialog()

wget for windows

ever wanted to use wget in Powershell? This short function allows you to wget to local directory.

function wget($urlpath){
  $directory = (get-location).path

  $filename = $urlpath.Split("/")[-1]
  $file = $directory+"\"+$filename

  $webclient = New-Object System.Net.WebClient
  $webclient.DownloadFile($urlpath,$file)
}

Simply enter the function into your current powershell session and try it out.
wget http://upload.wikimedia.org/wikipedia/en/f/f4/The_Scream.jpg

If you want to permanently have access to this command add the function to the following file: “%windir%\system32\WindowsPowerShell\v1.0\profile.ps1

For more information on Powershell profiles, read this article:
http://msdn.microsoft.com/en-us/library/windows/desktop/bb613488(v=vs.85).aspx

Powershell auto updater

I wrote this short script to keep local scripts up to date with server versions.

Makes it easier to handle scripts that are added with initial version through task sequence or other onetime distributions. The requirement is for the scripts to have the variable $CurrentVersion = <int/double>

Example script, located on the central/repository:

$CurrentVersion = 1.1
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$form = New-Object 'System.Windows.Forms.Form'
$form.showdialog()

If the local/current script contains $CurrentVersion = 1.0 or less it will get updated once the launcher has been run.
Launcher code below:

# Following variables control the paths used in script

# Retrieve the directory of script.
$Script:G_runDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# Name of the script to update.
$Script:G_Script  = "ScriptPath.ps1"

# Where repository version is located.
$Script:G_RepositoryPath = "\\$env:userdnsdomain\Path\To\Script"

# Build paths for scripts. Do not change!
$Script:G_LocalScript = "$G_runDir\$G_Script"
$Script:G_CentralScript = "$G_RepositoryPath\$G_Script"
===========================================================================

Function MainFunction(){

    if(Test-Path $G_CentralScript){

        $LocalInfo = Get-Info -ScriptPath $G_LocalScript
        $CentralInfo = Get-Info -ScriptPath $G_CentralScript
        
        $LocalVersion = $LocalInfo.Version
        $LocalChecksum = $LocalInfo.Checksum

        $CentralVersion = $CentralInfo.Version
        $CentralChecksum = $CentralInfo.Checksum

        Write-Host "Local Version: $LocalVersion. Central Version: $CentralVersion"
        
        If($LocalChecksum -ne $CentralChecksum){
            If($LocalVersion -ge $CentralVersion){
                Write-Host "Local script corrupt"
            }
            try{
                Write-Host "Updating script"
                Copy-Item "$G_CentralScript" -Destination "$G_runDir" -Force
                Write-host "Script pdated"
            }
            catch{
                Write-Error $_
            }
        }Elseif($LocalChecksum -eq $CentralChecksum){
            Write-Host "Script up to date"
        }
    }    
    Else{
        Write-host "Local Version: $LocalVersion. Could not find $G_CentralScript."
    }
       
    Launch-Application
} # End Function

#----------------------------------------------------------------
# Gets version of scripts.
#----------------------------------------------------------------
Function Get-Info{
    Param(
        [Parameter(Mandatory=$true)]$ScriptPath
    )
    
    $Return = @{"Checksum" = "Null";"Version" = 0}
    $MD5 = New-Object 'System.Security.Cryptography.MD5CryptoServiceProvider'
    
    If(Test-Path $ScriptPath){

        Try{
            $Checksum = [System.BitConverter]::ToString($MD5.ComputeHash([System.IO.File]::ReadAllBytes($ScriptPath)))
            $Return.Checksum = $Checksum
        }
        Catch{
            Write-Error $_
        }

        $Script = (get-content $ScriptPath) | Where-Object{$_ -like "*APP_VER*"}
        if(!$Script){
            Return Write-Host "$ScriptPath does not contain any valid version information!"
        }
        else{
            Try{
                # Uses invoke-expression to set version variable.
                $ScriptBlock = $Script | Where-Object{$_.StartsWith("Set-Variable")}
                If($ScriptBlock.GetType().Name -eq "String"){
                    Invoke-Expression $ScriptBlock
                    If(($APP_VER.GetType() -eq [Double]) -or ($APP_VER.GetType() -eq [Int])){
                        $Return.Version = $APP_VER
                    }
                }
            }Catch{
                Write-Error $_
            }
        }
    }
    Return $Return
}

#----------------------------------------------------------------
# Prompts for input and launches script.
#----------------------------------------------------------------

Function Launch-Application{
    Try{
        .{Powershell.exe -File "$Script:G_LocalScript"}
    }
    Catch{
        Write-Error $_
    }
} # End function

#=== Call Main function =========================================================
#================================================================================

MainFunction

Just copy-paste this and change the paths and you should be good. If you’re running a console application you might want to change the “-Windowstyle” in Start-Script function to “normal”
Currently it waits until any key has been pressed but if you want it to launch instantly just comment out the line

[void]$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")