(Note: These solutions were written for Event 8.) 

Beginner Event 8: The pole vault

In the pole vault event, you will raise the bar as you run down the folder that is consuming the most disk space on your computer.

Guest commentator: Michael Frommhold

Image of guest commentator Michael Frommhold 

Michael is a premier field engineer at Microsoft Germany.


VBScript solution

Archimedes gave the hint; Dr. Watson was supposed to do the job. Finding the folder consuming the most disk space should be easy said Dr. Watson. You connect to the drive that contains the folder you wish to scan by using the GetFolder method from the FileSystemObject. Next you use a query to obtain the size of the subfolders by using the subfolders property. At this point you can also obtain the size of the folder from the size property of the folder object.

Store the path to the largest folder in a variable named sWinner and the amount of disk space in the dbFileSize variable. Now you compare each new folder size with the one who claimed to be the largest one up to now. When enumerated all folders, convert the size from bytes to a readable number. This is done via Sub Handlesize. Our first attempt at solving the Beginner Event 8 is BeginnerEvent8Solution_1.vbs script, which is seen here.

BeginnerEvent8Solution_1.vbs

On Error Resume Next

Dim oFSO 
Dim oFolder
Dim oSubFolder
Dim sPath
Dim dbFileSize
Dim dbCtrl
Dim sWinner
Dim sUnit

dbFileSize = CDbl(0)

Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFolder = oFSO.GetFolder("\")

For Each oSubFolder In oFolder.SubFolders
          sPath = vbNullString
          sPath =  oSubFolder.Path
         
          If Not Len(sPath) = 0 Then
                   dbCtrl = CDbl(0)
                   dbCtrl = CDbl(oSubFolder.Size)
                  
                   If dbCtrl > dbFileSize Then
                             dbFileSize = dbCtrl
                             sWinner = sPath
                   End If
          End If
Next 'oSubFolder
         
Set oFolder = Nothing
         
HandleSize dbFileSize, sUnit

WScript.Echo "Archimedes says: Lever thrown!"
WScript.Echo "The folder consuming the largest amount of disk space is:"
WScript.Echo vbTab & sWinner & " : " & dbFileSize & " " & sUnit

Sub HandleSize(ByRef dbRet, ByRef sRet)
         
          On Error Resume Next
         
          If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "KB"
          End If
                  
                   If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "MB"
          End If
         
          If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "GB"
          End If

          'just for completeness
          If CDbl(dbRet / 1024) > 1 Then
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "TB"
          End If
         
          dbRet = Round(dbRet, 1)
         
End Sub

When the BeginnerEvent8Solution_1.vbs script is run, the output displayed in the following image is shown (keep in mind the red background seen here is a product of my own personal laptop configuration, and not a result of anything in the script):

Image of the output of the script 

There are a few things that could be improved in the BeginnerEvent8Solution_1.vbs script.

If you do not have access to any subfolder of the first-level subfolders, you will not get any value for the folder size, even if you would have access after agreeing to a UAC dialogue. This is because VBScript is not UAC aware.

If there are any junction points or symbolic links on your drive, you will scan these as well. This could result in a larger size that is actually larger than the total disk space of your drive.

So, what's the proper solution? Consulting Archimedes, we find that he tells us to find a sufficiently large lever. When working with VBScript perhaps the biggest lever we have is WMI. We query the Win32_Directory class from the root\cimV2 namespace, and handle the returned directories. The Win32_Directory class has a property called FileSize but unfortunately the FileSize property is always null. The reason is that a directory is an entrypoint for a collection of files and directories and this entrypoint has no size.

We will need to use another WMI class, the CIM_DataFile, which can be associated with a Win32_Directory object to obtain the FileSize for files. Now all we have to do is add all FileSize values in a directory and we have the size of the directory.  We also need to add the queried FileSize of any folder to his parent and to the parent of his parent and…you get the point.


To do this we can use a dictionary object with pairs of folder paths and folder sizes. For every new folder we process, the dictionary will be checked for parents and grandparents and so on.  The size will be added to the right side of the pair of the elders. As an add-on to the script, we save the pairs in another dictionary as well, but this time we only save the amount of used space in the directories root.

When we are finished with enumerating the drive, the pair with the highest number will be the folder consuming the most disk space.

Be sure to start the script in an administrative console. The drive to be scanned has to be passed as argument /disk to the script. You can optionally scan a remote machine (but due to performance considerations I wouldn't do it). Verbose output shows you that each folder scanned each and every updated dictionary pair. It is therefore very verbose.

You will find further information about the details of the script inline as comments. The complete BeginnerEvent8Solution_2.vbs script is seen here.

BeginnerEvent8Solution_2.vbs

'=====================================================================
' Usage: cscript lever.vbs /disk:<disk to scan>
'                                                                  {Optional /comp:<targetmachine> /dbg:True}
'=====================================================================

'handle errors when you need to
On Error Resume Next

'_____________________
' #region declarations

'constants
Const SUCCESS = 0
Const ERROR_WMI = 1
Const ERROR_WQL_DIR = 2
Const ERROR_DISK_ACCESS = 3
Const ERROR_NO_DRIVE = 4

Const WbemAuthenticationLevelPktPrivacy = &h6

'WMI stuff
Dim oWMILoc                  'As SwbemLocator
Dim oWMI           'As SwbemService

'store arguments
Dim sMachine       'As String
Dim sDrive                   'As String
Dim blOutput       'As Boolean

'misc
Dim aDrives                  'As String()
Dim iCount                   'As Integer

Dim dcOverAll      'As Scripting.Dictionary
Dim dcList                   'As Scripting.Dictionary

Dim sMSG           'As String

Dim sOverall       'As String
Dim sList          'As String

' #endregion

'_____________
' #region Main

'we're starting
WScript.Echo Now
WScript.Echo "Throwing the lever" & VbCrLf

'init return code
sMSG = vbNullString

'init dictionaries
Set dcOverAll = CreateObject("Scripting.Dictionary")
Set dcList = CreateObject("Scripting.Dictionary")

'_________________________
'anything to take care of?
ReadArguments sMachine, sDrive, blOutput

'____________________
'check WMI connection
If Not ConnectWMI(sMachine) Then _
          HastaLaVistaDotCom ERROR_WMI, _
                                                          "Failed to connect WMI!"

'____________________________
'told me which drive to scan?
If Len(sDrive) = 0 Then _
          HastaLaVistaDotCom ERROR_NO_DRIVE, _
                                                                   "No drive to scan given!"
         
'drive accessable?
If Not CheckAccess(sDrive) Then _
          HastaLaVistaDotCom ERROR_DISK_ACCESS, _
                                                                   "Access to " & sDrive & " is not granted!"
         
'__________
'scan drive
If Not WalkDirectories(sDrive, dcOverAll, dcList, sMSG) Then _
          HastaLaVistaDotCom ERROR_WQL_DIR, _
                                                          "Failed to query for directories: " & sMSG

'_______________
'process results
TheWinnerIS dcOverAll, dcList
         
'__________________________
'we're done -> big clean up
HastaLaVistaDotCom SUCCESS, vbNullString

' #endregion

'__________________________________
' #region functions and subsequents

'get all directories in given partition
Function WalkDirectories(ByVal sDisk, _
                                                          ByRef dcWinners, _
                                                          ByRef dcDirs, _
                                                         ByRef sRet) 'As Boolean
         
          On Error Resume Next
         
          Dim sWQL           'As String
          Dim colDirs                  'As Collection
          Dim oDir           'As Object
         
          Dim dbFileSize     'As Double
         
          Dim sFolder        'As String
         
          WalkDirectories = True
         
          sWQL = "SELECT Name FROM Win32_Directory WHERE Drive = '" & sDisk & "'"
         
          If blOutput Then WScript.Echo sWQL
         
          Set colDirs = oWMI.ExecQuery(sWQL)
                  
                   If colDirs Is Nothing Then
                  
                             sRet = sWQL & " :: " & Err.Description : WalkDirectories = False
                             sWQL = vbNullString : Exit Function
                            
                   End If
                  
                   'walk directories
                   For Each oDir In colDirs
                            
                             'get added up file size in dir
                             '(dirs are just collections of files and subfolders
                             'they do not have sizes)!
                   HandleDirectory oDir.Name, dbFileSize, sMSG
                  
                   'add dir to collection cverall
                             dcWinners.Add oDir.Name, dbFileSize
                             'add dir to collection for single info
                             dcDirs.Add oDir.Name, dbFileSize
                            
                             If blOutput Then WScript.Echo oDir.Name & " " & dbFileSize
                            
                             'is scanned dir a subfolder or subsub...folder of any
                             'of the already scanned dirs(?) -> add size
                             For Each sFolder In dcWinners.Keys
                                     
                                      If InStr(oDir.Name & "\", sFolder) > 0 Then
                                               
                                                dcWinners.Item(sFolder) = dcWinners.Item(sFolder) + dbFileSize
                                               
                                                If blOutput Then _
                                                          WScript.Echo sFolder & " " & dcWinners.Item(sFolder)
                                               
                                      End If
                                     
                             Next 'sFolder
                  
                   Next 'oDir
                  
          Set colDirs = Nothing
         
          'lil clean up
          sWQL = vbNullString
         
End Function

'walk files in directory and add up file sizes
Function HandleDirectory(ByVal sPath, _
                                                          ByRef dbRet, _
                                                          ByRef sRet) 'As Boolean
         
          On Error Resume Next
         
          Dim colFiles       'As Collection
          Dim oFile          'As Object
         
          dbRet = CDbl(0)
         
          'get all files in given dirtectory
          Set colFiles = oWMI.ExecQuery("ASSOCIATORS OF " & _
                                                                             "{Win32_Directory.Name='" & sPath & "'} " & _
                                                                             "WHERE resultClass = CIM_DataFile")
                  
                   For Each oFile In colFiles
                            
                             'add up file size
                             dbRet = dbRet + CDbl(oFile.FileSize)
                            
                   Next 'oFile
                  
          Set colFiles = Nothing
         
End Function

'partition is accessable (ex: not BitLocked?)
Function CheckAccess(ByVal sInput) 'As Boolean
         
          On Error resume Next
         
          Dim colRet                   'As Collection
          Dim oRet           'As Object
          Dim sRet           'As String
         
          sRet = vbNullString
         
          CheckAccess = True
         
          'can I read the given drive?
          Set colRet = oWMI.ExecQuery("SELECT FileSystem " & _
                                                                   "FROM Win32_LogicalDisk " & _
                                                                   "WHERE Name = '" & sInput & "'")
                  
                   For Each oRet In colRet
                            
                             sRet = oRet.FileSystem      
                            
                   Next 'oRet
                  
          Set colRet = Nothing 
         
          If Len(sRet) = 0 Then CheckAccess = False
         
          'lil clean up
          sRet = vbNullString
         
End Function
         
'connect WMI using SwbemLocator + Security_
Function ConnectWMI(ByVal sInput) 'As Boolean
         
          On Error Resume  Next
         
          ConnectWMI = True
         
          Set oWMILoc = CreateObject("WbemScripting.SwbemLocator")
                  
                   ' AuthLevel 0 - 5 + encrypts the argument value of each remote procedure call
                   '(see http://msdn.microsoft.com/en-us/library/aa393972(VS.85).aspx)
                   oWMILoc.Security_.AuthenticationLevel = WbemAuthenticationLevelPktPrivacy
                  
                   'I really want to know it!           
                   oWMILoc.Security_.Privileges.AddAsString "SeBackupPrivilege", True
         
          'connect to SwbemService on given machine
          Set oWMI = oWMILoc.ConnectServer(sInput, "root\cimv2")
         
          'failed?
          If oWMI Is Nothing Then ConnectWMI = False
         
End Function

'the Oscar goes to...
Sub TheWinnerIS(ByVal dcWinners, _
                                      ByVal dcDirs)
         
          On Error Resume Next
         
          Dim dbWinner       'As Double
          Dim dbDir                    'As Double
          Dim sWinner                  'As String
          Dim sDir           'As String
          Dim sKey           'As String
          Dim sUnit          'As String
         
          dbWinner = CDbl(0) : dbDir = CDbl(0)
         
          'walk overall collection
          For Each sKey In dcWinners.Keys
                  
                   'ok - I won't tell you, that the drive root is the largest 'folder'...
                   If Len(sKey) > 3 Then
                            
                             If dbWinner < dcWinners.Item(sKey) Then
                            
                                      dbWinner = CDbl(dcWinners.Item(sKey))
                                      sWinner = sKey
                                     
                             End If 'dbWinner < dcWinners.Item(sKey
                            
                   End If 'Len(sKey) > 3
                  
          Next 'sKey
         
          'you want to know KB, or MB or GB... and not Bytes, I guess
          HandleSize dbWinner, sUnit
         
          'return overall winner
          sWinner = sWinner & " : " & dbWinner & " " & sUnit
         
          'walk single info collection
          For Each sKey In dcDirs.Keys
                  
                   '...see last for each
                   If Len(sKey) > 3 Then
                            
                             If dbDir < dcDirs.Item(sKey) Then
                            
                                      dbDir = CDbl(dcDirs.Item(sKey))
                                      sDir = sKey
                                     
                             End If 'dbWinner < dcWinners.Item(sKey
                            
                   End If 'Len(sKey) > 3
                  
          Next 'sKey
         
          'you want to know KB, or MB or GB... and not Bytes, I guess
          HandleSize dbDir, sUnit
         
          'return overall winner
          sDir = sDir & " : " & dbDir & " " & sUnit
         
          'display results
          WScript.Echo vbNullString
          Wscript.Echo "Archimedes says: Lever thrown!"
          Wscript.Echo "The folder consuming the largest amount of diskspace is:"
          WScript.Echo vbTab & sWinner
          WScript.Echo "The folder with the most used file space in his root is:"
          WScript.Echo vbTab & sDir
         
          'lil clean up
          sKey = vbNullString : sUnit = vbNullString
          sWinner = vbNullString : sDir = vbNullString
          dbWinner = 0 : dbDir = 0
         
End Sub  

'return readable file size
Sub HandleSize(ByRef dbRet, _
                                      ByRef sRet)
         
          On Error Resume Next
         
          If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "KB"
                  
          End If
                  
                   If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "MB"
                  
          End If
         
          If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "GB"
                  
          End If
          CDbl
          'just for completeness
          If CDbl(dbRet / 1024) > 1 Then
                  
                   dbRet = CDbl(dbRet / 1024)
                   sRet = "TB"
                  
          End If
         
          dbRet = Round(dbRet, 1)
         
End Sub

'what did you tell me to do?
Sub ReadArguments(ByRef sBox, _
                                                ByRef sDisk, _
                                                ByRef blDbg)
         
          Dim oNamedArgs     'As WshNamed
         
          'egt given arguments
          Set oNamedArgs = WScript.Arguments.Named       
                  
                   'did you name a remote machine?
                   sBox = "."
                   If oNamedArgs.Exists("comp") Then sBox = oNamedArgs.Item("comp")
                  
                   'told me about a disk to scan?
                   sDisk = vbNullString
                   If oNamedArgs.Exists("disk") Then
                            
                             sDisk = oNamedArgs.Item("disk")
                             'forgot the : ?
                             If Len(sDisk) = 1 Then sDisk = sDisk & ":"
                            
                   End If
                  
                   'should I show you verbose output?
                   blDbg = False
                   If oNamedArgs.Exists("dbg") Then
                  
                             If UCase(oNamedArgs.Item("dbg")) = "TRUE" Then
                                     
                                      blDbg = True
                                     
                             Else
                                     
                                       blDbg = False
                                     
                             End If
                            
                   End If
         
          Set oNamedArgs = Nothing
         
End Sub

'clean up 'n' leave
Sub HastaLaVistaDotCom(ByVal iQuit, _
                                                          ByVal sQuit)
         
          WScript.Echo sQuit
          WScript.Echo Now
         
          Set oWMI = Nothing : Set oWMILoc = Nothing
          Set dcOverAll = Nothing : Set dcList = Nothing
          sMachine = vbNullString : sDrive = vbNullString
          sMSG = vbNullString
          blOutput = 0 : iCount = 0
          ReDim aDrives(0)
         
          WScript.Quit(iQuit)
         
End Sub

' #endregion

When you run the BeginnerEvent8Solution_2.vbs script, this output is shown:

Image of the output of the script


Guest commentator: Clint Huffman

Image of guest commentator Clint Huffman

Clint is best known for the Performance Analysis of Logs (PAL) tool (a VBScript), which simplifies the analysis of performance monitor logs. He is an author of many of the recent BizTalk performance guides on MSDN and recently spoke at TechEd 2008 about BizTalk performance analysis. Clint has appeared on RunAs Radio, and he maintains the Counter of the Week blog on TechNet.


VBScript solution

While I was working on the Summer Scripting Games Event 8 (the advanced event), I saw the details for the beginner division. Because I love to write scripts, I decided to go ahead and take a swat at it. Unlike my colleague Michael, I decided to stick with the FileSystemObject object.

The first thing I do is create an instance of the FileSystemObject and initialize a few variables. I then call the EnumFolders subroutine and pass in the path to the starting folder. I use the Wscript.Echo to display the path to the folder, as well as the size of the largest folder in megabytes. The size is determined by using the ConvertBytesToMegabytes function. The complete BeginnerEvent8Solution.vbs script is seen here.

BeginnerEvent8Solution.vbs

Option Explicit
Dim oFSO, oFolder, oSubFolder, sStartingFolder
Dim oLargestFolder

sStartingFolder = "C:\Program Files"
Set oFSO = CreateObject("Scripting.FileSystemObject")
oLargestFolder = Null

EnumFolders sStartingFolder
WScript.Echo oLargestFolder.Path
WScript.Echo ConvertBytesToMegaBytes(TotalSizeOfFiles(oLargestFolder)) & "MB"

Sub EnumFolders(sFolderPath)
    Set oFolder = oFSO.GetFolder(sFolderPath)
    If sFolderPath <> sStartingFolder Then
        Set oLargestFolder = ReturnTheLargestFolder(oLargestFolder, oFolder)
    End If
    For Each oSubFolder in oFolder.SubFolders
        EnumFolders oSubFolder.Path
    Next
End Sub

Function ConvertBytesToMegaBytes(iNumber)
    Dim iNumInMBs   
    iNumInMBs = CInt((iNumber / 1024) / 1024)
    iNumInMBs = FormatNumber(iNumInMBs, 0)
    ConvertBytesToMegaBytes = iNumInMBs
End Function

Function TotalSizeOfFiles(oFolder)
    Dim oFile, iFolderSize
    iFolderSize = 0
    For Each oFile in oFolder.Files
        iFolderSize = iFolderSize + oFile.Size
    Next
    TotalSizeOfFiles = iFolderSize
End Function

Function ReturnTheLargestFolder(oFolderA, oFolderB)
    If IsNull(oFolderA) = True AND IsNull(oFolderB) = False Then
        Set ReturnTheLargestFolder = oFolderB
        Exit Function
    End If
    If TotalSizeOfFiles(oFolderA) > TotalSizeOfFiles(oFolderB) Then
        Set ReturnTheLargestFolder = oFolderA
    Else
        Set ReturnTheLargestFolder = oFolderB
    End If
End Function


Guest commentator: Brandon Shell

Image of guest commentator Brandon Shell

Brandon is a Microsoft MVP, moderator for the Official Scripting Guys forum. He maintains a personal blog named BSonPoSH.

Windows PowerShell solution

This specific script was born out of necessity. I had a Terminal Server that was running out of space and I needed to determine where all the space was being used. I started doing this manually and found that it was the “Documents and Settings” folder.  As this server had 100s of users and the idea of right-clicking my way through each one to find out which user was taking the most space was a little daunting.

My initial approach for that project was only to address the problem at hand and that led to a simple script that basically got all the folders in the “Documents and Settings” folder and calculate the size to see what users I should yell at about cleaning up their directories. I started by collecting all the folders into a variable. I then processed each folder individually. The processing included getting all the files recursively and calculating the total size by using the Measure-Object cmdlet and creating a custom object ($myobj) for the folder. The final step was to collect the custom objects into an array ($mycol).

After I put out my immediate fire, I decided to add some of the cool features of the script. First I wanted to add the ability to override the default behavior and include hidden folders. To accomplish this I needed to be able to take a –Force parameter passed to the parent script and pass it to the Get-ChildItem call. The default behavior of the script is only to get the folder size of the folders in the path passed to the script. I thought it would be useful to be able to recursively get all the folder sizes so I added a –Recurse parameter. This allows you do get all the folders in the path and then calculate the folder sizes from there.

I have since used this script numerous times to clean up servers and even my personal laptop. The modified script is the BeginnerEvent8Solution.ps1 script, which is seen here.

BeginnerEvent8Solution.ps1

Param($path = $pwd,[switch]$force,[switch]$recurse)
$ErrorActionPreference = "SilentlyContinue"
$folders = get-childitem $path -force:$force -recurse:$recurse | where-object{$_.PSIsContainer}
$mycol = @()
foreach($folder in $folders)
{
   Write-Host "Processing Folder $folder"
   $myObj = "" | Select Name,SizeMB
   [int]$DirSize = "{0:n2}" -f (((Get-Childitem $folder.FullName -recurse -force | measure-object -sum Length).Sum)/1mb)
   $myobj.Name = $folder.Name
   $myobj.SizeMB = $DirSize
   $mycol += $myobj
}

$mycol | sort-object SizeMB -Desc | format-table -auto


Advanced Event 8: The pole vault

In the pole vault event, you will soar over your goals as you write an activity tracker script.

Guest commentator: Clint Huffman

VBScript solution

This script can be run under either CScript or WScript. It will ask (nag) the user every hour for a log entry, and then write the answer to a log file. The log file is automatically created each day. It puts a date-time stamp in the log for each entry. If you do not put anything in the InputBox function, click Cancel or click Close, the script will ask if you if you really want to quit the script or not.

Now that I am done with the script, I think I will actually use this myself from now on. One improvement would be to create a larger text field. Unfortunately, this is something I can’t modify in VBScript as the InputBox function is limited to approximately 1024 characters. Windows PowerShell would be able to do a better job by invoking a .NET Form object. The AdvancedEvent8Solution.vbs script is seen here.

AdvancedEvent8Solution.vbs

Option Explicit
Const ONE_HOUR = 3600000
Const ForAppending = 8

'// Declaring the variables
Dim sDateTime,sLogEntry,sLine
Dim oFSO, oFile
Dim iQuit
Dim bNeverTrue

bNeverTrue = False
Set oFSO = CreateObject("Scripting.FileSystemObject")

'// Loop forever
Do Until bNeverTrue = True
    sDateTime = GetDate()
    '// Nag for what you are going now?
    sLogEntry = InputBox("What are you doing now?", "Activity Logger", "", 0,0)
    If sLogEntry = "" Then '// If a blank response was received, Cancel was clicked, or the Close button was clicked.
        '// Ask if you are sure if you want to quit the script? Otherwise, continue normal execution.
        iQuit = MsgBox("Are you sure you want to quit?", vbYesNo, "Activity Logger")
        If iQuit = vbYes Then
            '// Yes was selcted. Quit the script.
            WScript.Quit
        End If
    End If
    sLine = "[" & Now() & "] " & sLogEntry '// Add the date time to the front of the log entry.
    '// Open today's log file or create a new one if it doesn't exist.
    Set oFile = oFSO.OpenTextFile(sDateTime & "_ActivityLog.log", ForAppending, True)
    oFile.WriteLine sLine '// Write the log entry.
    oFile.Close '// Close the file, so any handles on the file are closed.
    WScript.Sleep ONE_HOUR '// Sleep for one hour or a specified time in milliseconds.
Loop

Function GetDate()
    '// Get the current date in Year Month Day (yyyyMMdd) format.
    GetDate = Year(Now) & ZeroPad(Month(Now),2) & ZeroPad(Day(Now),2)
End Function

Function ZeroPad(sString, iLength)
    '// Pad the a string with zeroes.
    ZeroPad = string(iLength - len(sString),"0") & sString
End Function


Guest commentator: Lee Holmes

Image of guest commentator Lee Holmes

Lee Holmes is a developer on the Microsoft Windows PowerShell team, author of the Windows PowerShell Cookbook, and Windows PowerShell Quick Reference. He also runs the Precision Computing Blog.


Windows PowerShell solution

Activity Tracker

A light-weight personal productivity tool

Activity Tracker helps you analyze your time by infrequently asking the simple question: “What are you doing?

Image of Activity Tracker


How it works

Activity Tracker follows the same principles as a traditional software sampling profiler, but instead samples humans. By randomly recording your current task, Activity Tracker lets you analyze your answers as a fairly faithful proxy for how you actually spent your time. If 20 percent of your answers were “Status Meeting,” then you spent close to 20 percent of your time in status meetings.

An alternative to sampling

An alternative to the sampling approach is an instrumentation approach: faithfully recording your transition between tasks. Activity Tracker avoids this design, because asking humans to faithfully record transitions between tasks is enormously error prone. For example, you might not log a task transition for a task that you consider inconsequential (for example, “Checking email”) when in fact that task may account for a significant portion of your day. Some software attempts to address the human element by tracking window titles, but the level of data captured by window titles often does not map well to the task they support.

Using Activity Tracker

Activity Tracker is a Windows PowerShell script. It spawns a new instance of Windows PowerShell to run itself, but also lets you specify the –AsApplication flag if you want it to have a unique name for the resulting .exe file. This new executable is simply a copy of PowerShell.exe.

Once launched, Activity Tracker sits in the background. Once in awhile (randomly, between 5 and 25 minutes), it asks you the question, “What are you doing?” It stores your previous answers in a list until you exit the program, which lets you easily re-use your answers to previous questions.

When you press OK, it adds your answer (along with the current window title) to a file in “My Documents\ActivityTracker”one file per week. The file is named to correspond to the date on the first day of the week.

If you don’t answer within four minutes, it dismisses the dialog and checks your Office Outlook calendar. If you are in a meeting, it records the title of that meeting. If you aren’t in a meeting, it records nothing. This lets you keep the Activity Tracker running when you go home for the day without polluting your journal files.

Slicing and dicing

The Activity Tracker records its output as a simple CSV file. Knowing that, you can slice and dice results to your heart’s content. For example, to easily get a summary of your week:                                                                

PS >Import-Csv temp.csv | Group Activity | Sort -Descending Count         
                                                                           
Count Name                      Group                                     
----- ----                      -----                                     
   23 Hubble Space Telecsope... {@{Date=5/20/2009 8:24:19 AM; WindowTit...
    8 Meeting: Design review    {@{Date=5/20/2009 1:10:21 PM; WindowTit...
    5 Meeting: Team meeting     {@{Date=5/20/2009 3:10:20 PM; WindowTit...
    4 Email                     {@{Date=5/20/2009 8:04:26 AM; WindowTit...
    3 Scripting games           {@{Date=5/19/2009 6:09:16 PM; WindowTit...

                                                                                                                                              

To count how many hours you spent on a task, simply divide by four.

Activity tracker uses the PowerBoots UI scripting library, which is available from CodePlex.  

AdvancedEvent8Solution.ps1

#requires -version 2
##############################################################################
##
##   Start-ActivityTracker
##   by Lee Holmes - http://www.leeholmes.com/blog
##   Guest entry for the 2009 Scripting Games
##
##############################################################################

##############################################################################
##
## .SYNOPSIS
##   Helps you track your time by infrequently asking the simple question:
##   "What are you doing?" The Activity Tracker records your responses in a
##   CSV in the "ActivityTracker" directory under your "My Documents"
##   directory, broken down by week.
##
## .EXAMPLE
##   # Launch the Activity Tracker
##   C:\PS>Start-ActivityTracker
##
##   # Launch the Activity Tracker, but as a standalone application
##   # called "ActivityTracker.exe"
##   C:\PS>Start-ActivityTracker -AsApplication
##
##   # Launch from the start menu / shortcut
##   powershell -NoProfile -Command Start-ActivityTracker -AsApplication
##
##############################################################################

param(
    ## Launches Activity Tracker as its own .exe. This is useful if you don't
    ## want the prompt window to get grouped with other Windows PowerShell windows.
    [switch] $AsApplication,

    ## Internal switch used to spawn the activity tracker as another instance
    ## of Windows PowerShell that persists even when your current shell closes.
    [switch] $UseCurrentShell
)

Set-StrictMode -Version Latest

## Verify PowerBoots is available
if(-not (Get-Module PowerBoots -List))
{
    $error = "Activity Tracker requires the PowerBoots module. " +
        "Please visit http://powerboots.codeplex.com/ to install."
    throw $error
}

## And that we are not running on CTP3
if($PSVersionTable.BuildVersion -le "6.1.6949.0")
{
    $downloadUrl = "http://blogs.msdn.com/powershell/pages/" +
        "download-windows-powershell.aspx"
    $error = "Activity Tracker requires a version of PowerShell more recent" +
        " than CTP3. Please visit $downloadUrl to install."
    throw $error
}

## If the user wants to launch Activity Tracker as a separate EXE, copy
## PowerShell.exe to ActivityTracker.exe
$applicationToLaunch = "PowerShell"

if($asApplication)
{
    $targetExe = "$env:TEMP\ActivityTracker.exe"
    $SCRIPT:applicationToLaunch = $targetExe
 
    if(-not (Test-Path $targetExe))
    {
        Copy-Item "$pshome\powershell.exe" $targetExe
    }
}

## If this is the first launch, relaunch ourselves with the -UseCurrentShell
## parameter. This launches a new hidden Windows PowerShell instance that runs the
## actual script.
if(-not $useCurrentShell)
{
    $file = $myInvocation.MyCommand.Path
    $arguments = "-NoProfile","-STA","-File ""$file""","-UseCurrentShell"
    Start-Process $applicationToLaunch `
        -ArgumentList $arguments -WindowStyle Hidden
       
    return
}

## Set our title
$host.UI.RawUI.WindowTitle = "Activity Tracker Console"

## Load and create our dependencies
Import-Module PowerBoots
Add-Type -Assembly UIAutomationClient

##############################################################################
##
##   General utility functions
##
##############################################################################

## Look at Office Outlook to determine your current meeting, if you're in one.
## This is used when you're away from your desk and the dismiss timer
## auto-dismisses the dialog.
function GLOBAL:Get-CurrentAppointment
{
    try
    {
        ## Access Outlook via COM, and open the Calendar folder
        $olApp = New-Object -com Outlook.Application
        $namespace = $olApp.GetNamespace("MAPI")
        $fldCalendar = $namespace.GetDefaultFolder(9)
        $items = $fldCalendar.Items

        ## Sort by start time, descending, so that we can access
        ## the most recent items first.
        $items.Sort("[Start]", $true)

        ## Look through all the items
        foreach($item in $items)
        {
            ## We're done if the current item ended in the past
            if($item.End -lt [DateTime]::Now)
            {
                return
            }

            ## If the current appointment spans the current time,
            ## AND is marked as "Busy" (as opposed to "Free,")
            ## then return that as a result.
            if(($item.Start -le [DateTime]::Now) -and
               ($item.End -ge [DateTime]::Now) -and
               ($item.BusyStatus -eq 2))
            {
                "Meeting: " + $item.Subject
            }
        }
    }
    catch
    {
        ## Catch the error when the user doesn't have Outlook
        ## installed.
        $error.RemoveAt(0)
    }
    finally
    {
        $olApp = $null
    }
}

## Get the log file for the current session. The name is derived from
## the date that represents the first day of the week. This lets the user
## easily review what they were doing for a given week.
function GLOBAL:Get-LogfileName
{
    param($weeks = 1)
   
    ## Get the current date, and scan backward until we find the first day
    ## of that week.
    $dateTimeFormat  = (Get-Culture).DateTimeFormat
    $weekStart = Get-Date

    while($weekStart.DayOfWeek -ne $dateTimeFormat.FirstDayOfWeek)
    {
        $weekStart = $weekStart.AddDays(-1)
    }

    ## Return the log file names for the last $weeks number of weeks
    while($weeks -gt 0)
    {
        ## Convert that to a file name
        $filename = $weekStart.ToString("yyyyMMdd") + ".csv"
        $mydocs = [Environment]::GetFolderPath("MyDocuments")
        Join-Path $mydocs "ActivityTracker\$filename"

        $weeks = $weeks - 1
        $weekStart = $weekStart.AddDays(-7)
    }
}

## Record an activity
function GLOBAL:RecordActivity($activity)
{
    if(-not $activity) { return }

    ## If the dialog was auto-dismissed, then find their current appointment.
    if($activity -eq "N/A")
    {
        $activity  = Get-CurrentAppointment

        if(-not $activity)
        {
            return
        }
    }

    ## Bring their current activity to the top of the list, so that it also
    ## acts as a most-recently-used list
    $items.Remove($activity)
    $null = $items.Insert(0, $activity)

    ## Create the output object
    $outputObject = New-Object PsObject -Property @{
        Date = Get-Date;
        WindowTitle = $GLOBAL:windowTitle
        Activity = $activity
    }

    ## Then record it in the log file. We either append or create, depending
    ## on if the file exists or not.
    $logfile = Get-LogfileName
    if(Test-Path $logfile)
    {
        ($outputObject | Select Date,WindowTitle,Activity |
            ConvertTo-Csv -NoTypeInformation)[-1] >> $logfile
    }
    else
    {
        $null = New-Item -Type File $logfile -Force
        $outputObject | Select Date,WindowTitle,Activity |
            Export-Csv $logFile -Encoding Unicode -NoTypeInformation
    }
}


##############################################################################
##
##   UI/application logic
##
##############################################################################


## Hold the list of items visible in the selection box, as well
## as all items recorded
$GLOBAL:visibleItems =
    New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$GLOBAL:items = New-Object System.Collections.ArrayList

## Create the timer used to auto-dismiss the dialog if you're away from
## the computer
$GLOBAL:dismissTimer = new-object System.Windows.Threading.DispatcherTimer
$dismissTimer.Interval = New-TimeSpan -Minutes 4
$dismissTimer.Add_Tick({
    $activity = Select-BootsElement $bootsWindow Activity
    $activity.Text = "N/A"
    OK_Click
})

## Called when the main window is loaded
function GLOBAL:On_Loaded
{
    ## Import any items that have been logged for the last 2 weeks. Sort
    ## them in reverse order, so that the most recent is at the top of the
    ## list.
    $logfiles = Get-LogfileName -Weeks 2 | Where-Object { Test-Path $_ }
    if($logfiles)
    {
        $importedItems = @(Import-Csv $logFiles | sort { [DateTime] $_.Date } |
            Foreach-Object { $_.Activity })
        [Array]::Reverse($importedItems)

        ## Only add the unique entries
        foreach($item in $importedItems)
        {
            if(-not $items.Contains($item))
            {
                $items.Add($item)
            }
        }
    }

    ## Refresh the UI with the new data, and set our window icon
    SendToBackground
   
    RefreshList
    Set-Icon

    ## Start the popup timer that will load the window once every
    ## 15 minutes.
    $GLOBAL:timer = new-object System.Windows.Threading.DispatcherTimer
    $GLOBAL:timer.Interval = New-TimeSpan -Minutes (Get-Random -Min 5 -Max 25)

    ## Remember that the user isn't typing a selection
    $GLOBAL:selectionTyping = $false
   
    $timer.Add_Tick( {

        ## Figure out the window title of the active application
        $focusedProcess = [Windows.Automation.AutomationElement]::FocusedElement.Current.ProcessId
        do
        {
            ## Some processes don't have a window title, but their parent does
            ## (for example, IE 8)
            $GLOBAL:windowTitle = (Get-Process -Id $focusedProcess).MainWindowTitle
            $parentProcess = Get-WmiObject Win32_Process -Filter "ProcessId=$focusedProcess"
            if($parentProcess)
            {
                $focusedProcess = $parentProcess.ParentProcessId
            }
            else
            {
                $focusedProcess = $null
            }
        } while((-not $GLOBAL:windowTitle) -and ($focusedProcess))

        ## Reschedule the next timer elapse randomly, so that the measurement
        ## is more accurate.
        $GLOBAL:timer.Interval = New-TimeSpan -Minutes (Get-Random -Min 5 -Max 25)
       
        ## When the timer elapses, unhide the window and flash it in the
        ## task bar
        $win32Utils = [Lee.Holmes.ActivityTracker.Win32Utils]
        $win32Utils::ShowWindow($bootsWindow.Handle)
        $win32Utils::Flash($bootsWindow)

        ## Refresh the UI to include all items
        RefreshList
       
        ## When we get keyboard focus, select the Activity window,
        ## pre-populated with the first item in the list
        $activity = Select-BootsElement $bootsWindow Activity

        $activity.Text = $visibleItems[0]
        $activity.SelectAll()
   
        $activity.Focus()

        ## And start the timer that will auto-dismiss the window
        $dismissTimer.Start()
    } )
    $timer.Start()
}

## Called when the user wants to submit an item
function GLOBAL:OK_Click
{
    $selectedItems = Select-BootsElement $bootsWindow SelectedItems
    $activity = Select-BootsElement $bootsWindow Activity

    ## If they select an item from the list, use that as the current
    ## activity.
    if($selectedItems.SelectedIndex -ge 0)
    {
        $result = $selectedItems.SelectedItem
    }
    ## Otherwise, use what they typed.
    else
    {
        $result = $activity.Text
    }

    ## Record the activity, refresh the list, and hide the window
    RecordActivity $result
    RefreshList
    SendToBackground

    ## Remember that they are no longer typing a selection
    $GLOBAL:selectionTyping = $false
}

## Called when the user double-clicks on an item
function GLOBAL:SelectedItems_DoubleClick
{
    ## Pull the activity from the clicked item, and submit that.
    $result = $_.OriginalSource.DataContext

    RecordActivity $result
    RefreshList
    SendToBackground
}

## Called when the user selects an item
function GLOBAL:SelectedItems_Select
{
    if(-not $GLOBAL:selectionTyping)
    {
        $result = $_.OriginalSource.SelectedValue
        $activity = Select-BootsElement $bootsWindow Activity

        $activity.Text = $result
        $activity.SelectAll()

        $activity.Focus()
    }
}

## Called when the user wants to skip tracking of this time
function GLOBAL:Dismiss_Click
{
    $activity = Select-BootsElement $bootsWindow Activity
    $activity.Text = "N/A"

    OK_Click
}

## Send the window to the background.
function GLOBAL:SendToBackground
{
    ## Hide it, minimize it, and stop the timer that auto-dismisses
    ## the window
    [Lee.Holmes.ActivityTracker.Win32Utils]::HideWindow($bootsWindow.Handle)
    $bootsWindow.WindowState = "Minimized"
    $dismissTimer.Stop()
}

## Refresh the list of visible items
function GLOBAL:RefreshList
{
    $visibleItems.Clear()
    $items | % { $visibleItems.Add($_) }
}

## Called when the user types in the "Activity" window. This filters the
## list of visible items as they type them.
function GLOBAL:Activity_KeyUp
{
    ## Get the activity text
    $filterText = $this.Text
   
    if(-not $filterText)
    {
        $filterText = ".*"
    }
   
    try
    {
        ## If this is a regex, do a regex match
        $visibleItems.Clear()
        $items -match $filterText | % { $visibleItems.Add($_) }
    }
    catch
    {
        ## If the regex was invalid, do simple text match
        $escaped = [System.Management.Automation.WildcardPattern]::Escape(
            $filterText)
        $simpleFilter = "*$escaped*"
        $visibleItems.Clear()
        $items -like $simpleFilter | % { $visibleItems.Add($_) }
    }

    ## Remember that the user is typing a selection to prevent our
    ## automatic text box updates when the selection changes
    $GLOBAL:selectionTyping = $true

    ## Now, select the first item that matched their text
    $selectedItems = Select-BootsElement $bootsWindow SelectedItems
    $selectedItems.SelectedIndex = 0
}


##############################################################################
##
##   General UI support functions
##
##############################################################################

## Create a DLL to support our Win32 P/Invoke calls
$assemblies = "PresentationCore","PresentationFramework","WindowsBase"
Add-Type -ReferencedAssemblies $assemblies @'
using System;
using System.Windows.Interop;
using System.Runtime.InteropServices;

namespace Lee.Holmes.ActivityTracker
{
    public static class Win32Utils
    {
        [DllImport("user32.dll")]
        private static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);

        public static void ShowWindow(IntPtr hWnd) {
            ShowWindowAsync(hWnd, 5);
        }
        public static void HideWindow(IntPtr hWnd) {
            ShowWindowAsync(hWnd, 0);
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool FlashWindowEx(ref FLASHWINFO pwfi);

        [StructLayout(LayoutKind.Sequential)]
        private struct FLASHWINFO
        {
            public uint cbSize;
            public IntPtr hwnd;
            public uint dwFlags;
            public uint uCount;
            public uint dwTimeout;
        }

        const uint FLASHW_ALL = 3;
        const uint FLASHW_TIMERNOFG = 12;

        public static void Flash(System.Windows.Window window)
        {
            FLASHWINFO fi = new FLASHWINFO();
            fi.cbSize = Convert.ToUInt32(Marshal.SizeOf(fi));
            fi.hwnd = (new WindowInteropHelper(window)).Handle;
            fi.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG;
            fi.uCount = 10;
            fi.dwTimeout = 0;
            FlashWindowEx(ref fi);
        }
    }
}
'@

## A function to set the icon for the form. This is a Base64-encoded version
## of the bytes for the GIF.
function GLOBAL:Set-Icon
{
    $encodedIcon = @"
R0lGODlhMABAAPcAAAAAAAUFBQsMCxESDhEREBQVEhYVFRgYFRkZGB0bGR4eHiAdGyIhHSokHiUkIyklIiknKC0pJCoqKiosNDItKjk0Ljo2ODk5Oj88Oj09
PUA2MkE8N0RAOEJCQURFSEpGQ09LR05MTlNMR1BMSFNRTVhSSV1YT1ZWVFpTUVpZVVxcW1xcYWJdVWJdW2RhVWBhXWxnW2xnXXBnVHBpVmVjYmllYWxoZmxr
anRvZXFsa3JwanJwbXRwbnFxcXRxcXVzdHZ2c3d3dXp2dH13eHt4c3p4dXx5dX18em58snWDs36KuIF+fISBfoiEfoKCgIOEgoSCgYWFgYaFhYeHiIeIhomG
g4qIhouMh4+Igo2IhYmJiYyKiY6NiY6OjY6OkpGNipCPk5KRjZiVj5KRkZOSlJaSkZSTlJWUkpWVlZiVkpiWlZyVkJuXmJqZlpmZmJucmZ2amZ2bnJycmp2d
nKGenKKhnaKioaOjpKShoaWkoqampKenqKeopamlo6ioo6ippayppq6tpqmpqKurrK2qqa2tqq2traqssq+xtbGuq7CusLKxrri3rbGxsbO0sbK0tLSysraz
tLW1srW1tLa1uLe4tLa4uLi2tLm5tbm5ubq6vbu8uLu+v7y7ub29ub29vZaewZifwpqiw6Oqx7y9wb7Awr3C0cC+vMC/wcHBvcHBwcLBxsPEwMTCwMXEwsXF
xcTFysfIxcbJzMjGw8jHysnIw8nIxs3JxsnJycnKzcvMyMjMz8zKyc7LzM3Nyc3NzcTI1MrM0s/Qzc7R1M7R2NDNzNDP1NHQzdLR0dHS1dPU0NLU19TS0NTS
1dXU0tXV1dPV2dbY29jW1djX2dnZ1tnZ2dna3dvc2dvc3d3a2d3d2t3d3dbZ4dvc4d/g393g5d7h6eDe3eDf4OHg3uHh4eHi5uPk4+Pk5eTj4+Xj5eTk4uXl
5eTk6efo6urn5ujn6+np6enp7evt7+zr6+zq7u3t7e3t8O7w8fDv8vHx8fLx9PX19fn5+f39/QAAAAAAACH5BAEAAP8ALAAAAAAwAEAAAAj/AP8JHEiwoMGD
CBMqXDhw3a9MlRpJegaPoUWL9WzZAdIFDRoqRf48u0jyILxPUQTt68ey37YoMWyVnPkvHy4r1Vrq7Ddmx0iaF9eledVyX76VLcekiQfUIio5LFVlMHAAAQMV
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
YxjByMUuZLGLXMhCFrmIBlP4jRB5gM0vAQEAOw==
"@

    ## Extract the bytes from the encoded format, and write them to an
    ## in-memory stream of bytes
    $bytes = [Convert]::FromBase64String($encodedIcon.Replace("`n", ""))
    $memoryStream = New-Object System.IO.MemoryStream $bytes.Count
    $memoryStream.Write($bytes, 0, $bytes.Count)
    $null = $memoryStream.Seek(0, "Begin")

    ## Convert the memory stream of bytes into a GIF
    $gifDecoder =
        New-Object System.Windows.Media.Imaging.GifBitmapDecoder(
            $memoryStream, "PreservePixelFormat", "Default"
        )
    $bitmapSource = $gifDecoder.Frames[0]

    ## Now apply the image as the form's icon
    $bootsWindow.Icon = $bitmapSource
}

## Set an attached property of an item (for example, the position of an
## element in a grid.)
function GLOBAL:SetProperty($name, $value)
{
    $element = @($input)[0]
   
    $class,$property = $name -split '\.'
    $type = [type] "System.Windows.Controls.$class"
    $method = "Set$property"
    $null = $type::$method.Invoke($element, $value)
    $element
}

## Get the scriptblock of an item. This lets Boots call the function with
## correct local variables.
function GLOBAL:Action($name)
{
    (Get-Command $name -CommandType Function).ScriptBlock
}

## Find an element in a WPF window
function GLOBAL:Select-BootsElement($window, $name)
{
    [System.Windows.LogicalTreeHelper]::FindLogicalNode($window, $name)
}

##############################################################################
##
##   UI Definition
##
##############################################################################

Boots -Title "Activity Tracker" -MinWidth 400 -MaxWidth 1000 -Height 300 `
        -WindowStartupLocation CenterScreen `
{
    GridPanel -Margin 5 -RowDefinitions @(
        RowDefinition -Height Auto
        RowDefinition -Height *
        RowDefinition -Height Auto
        RowDefinition -Height Auto
    ) {
        TextBlock -Margin 5 {
            "What are you doing? Type to search. Press Enter, OK, " +
                "or double-click an item to record it." } |
            SetProperty Grid.Row 0
       
        ScrollViewer -Margin 5 {
            ListBox -Name SelectedItems -FontFamily "Arial" `
                -ItemsSource (,$visibleItems) `
                -On_Select (Action SelectedItems_Select) `
                -On_MouseDoubleClick (Action SelectedItems_DoubleClick)
        } | SetProperty Grid.Row 1
       
        TextBox -Margin 5 -Name Activity -On_KeyUp (Action Activity_KeyUp) |
            SetProperty Grid.Row 2
       
        GridPanel -Margin 5 -HorizontalAlignment Right -ColumnDefinitions @(
            ColumnDefinition -Width 65
            ColumnDefinition -Width 10
            ColumnDefinition -Width 65
        ) {
            Button -IsDefault -Width 65 { "OK" } `
                -On_Click (Action  OK_Click) | SetProperty Grid.Column 0
            Button -Width 65 { "Dismiss" } `
                -On_Click (Action Dismiss_Click) | SetProperty Grid.Column 2
        } | SetProperty Grid.Row 3
        
   }
} -On_Loaded (Action On_Loaded)


Woohoo! What an awesome group of commentators! Thanks Michael, Clint, Brandon, and Lee for providing an entertaining and enriching collection of scripts. If you have not submitted your entries for Event 8, it is not too late. Maybe the commentators have given you a nudge in the right direction. See the blog for details about submitting entries for the Scripting Games. Don't forget, we are doing daily random drawings, but of course, you have to submit a script to qualify for one of the awesome prizes. Join us tomorrow for another round of awesome Scripting Games commentary—Event 9 (number 9, number 9, number 9).

 

Ed Wilson and Craig Liebendorfer, Scripting Guys