(Note: These solutions were written for Event 8.)
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.
Michael is a premier field engineer at Microsoft Germany.
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 NextDim oFSO Dim oFolderDim oSubFolderDim sPathDim dbFileSizeDim dbCtrlDim sWinnerDim sUnitdbFileSize = 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, sUnitWScript.Echo "Archimedes says: Lever thrown!"WScript.Echo "The folder consuming the largest amount of disk space is:"WScript.Echo vbTab & sWinner & " : " & dbFileSize & " " & sUnitSub 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):
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 toOn Error Resume Next'_____________________' #region declarations'constantsConst SUCCESS = 0Const ERROR_WMI = 1Const ERROR_WQL_DIR = 2Const ERROR_DISK_ACCESS = 3Const ERROR_NO_DRIVE = 4Const WbemAuthenticationLevelPktPrivacy = &h6'WMI stuffDim oWMILoc 'As SwbemLocatorDim oWMI 'As SwbemService 'store argumentsDim sMachine 'As StringDim sDrive 'As StringDim blOutput 'As Boolean'miscDim aDrives 'As String()Dim iCount 'As IntegerDim dcOverAll 'As Scripting.DictionaryDim dcList 'As Scripting.DictionaryDim sMSG 'As StringDim sOverall 'As StringDim sList 'As String' #endregion'_____________' #region Main'we're startingWScript.Echo Now WScript.Echo "Throwing the lever" & VbCrLf'init return codesMSG = vbNullString 'init dictionariesSet dcOverAll = CreateObject("Scripting.Dictionary")Set dcList = CreateObject("Scripting.Dictionary")'_________________________'anything to take care of?ReadArguments sMachine, sDrive, blOutput'____________________'check WMI connectionIf 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 driveIf Not WalkDirectories(sDrive, dcOverAll, dcList, sMSG) Then _ HastaLaVistaDotCom ERROR_WQL_DIR, _ "Failed to query for directories: " & sMSG'_______________'process resultsTheWinnerIS dcOverAll, dcList '__________________________'we're done -> big clean upHastaLaVistaDotCom SUCCESS, vbNullString' #endregion'__________________________________' #region functions and subsequents'get all directories in given partitionFunction 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 sizesFunction 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 sizeSub 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' leaveSub 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:
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 Tech∙Ed 2008 about BizTalk performance analysis. Clint has appeared on RunAs Radio, and he maintains the Counter of the Week blog on TechNet.
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 ExplicitDim oFSO, oFolder, oSubFolder, sStartingFolderDim oLargestFoldersStartingFolder = "C:\Program Files"Set oFSO = CreateObject("Scripting.FileSystemObject")oLargestFolder = NullEnumFolders sStartingFolderWScript.Echo oLargestFolder.PathWScript.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 NextEnd SubFunction ConvertBytesToMegaBytes(iNumber) Dim iNumInMBs iNumInMBs = CInt((iNumber / 1024) / 1024) iNumInMBs = FormatNumber(iNumInMBs, 0) ConvertBytesToMegaBytes = iNumInMBsEnd FunctionFunction TotalSizeOfFiles(oFolder) Dim oFile, iFolderSize iFolderSize = 0 For Each oFile in oFolder.Files iFolderSize = iFolderSize + oFile.Size Next TotalSizeOfFiles = iFolderSizeEnd FunctionFunction 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 IfEnd Function
Brandon is a Microsoft MVP, moderator for the Official Scripting Guys forum. He maintains a personal blog named BSonPoSH.
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
In the pole vault event, you will soar over your goals as you write an activity tracker script.
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 ExplicitConst ONE_HOUR = 3600000Const ForAppending = 8'// Declaring the variablesDim sDateTime,sLogEntry,sLineDim oFSO, oFileDim iQuitDim bNeverTruebNeverTrue = FalseSet oFSO = CreateObject("Scripting.FileSystemObject")'// Loop foreverDo 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.LoopFunction GetDate() '// Get the current date in Year Month Day (yyyyMMdd) format. GetDate = Year(Now) & ZeroPad(Month(Now),2) & ZeroPad(Day(Now),2)End FunctionFunction ZeroPad(sString, iLength) '// Pad the a string with zeroes. ZeroPad = string(iLength - len(sString),"0") & sStringEnd Function
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.
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?”
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 availableif(-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 CTP3if($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 dependenciesImport-Module PowerBootsAdd-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 activityfunction 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 loadedfunction 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 itemfunction 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 itemfunction 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 itemfunction 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 timefunction 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 itemsfunction 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 = @"R0lGODlhMABAAPcAAAAAAAUFBQsMCxESDhEREBQVEhYVFRgYFRkZGB0bGR4eHiAdGyIhHSokHiUkIyklIiknKC0pJCoqKiosNDItKjk0Ljo2ODk5Oj88Oj09PUA2MkE8N0RAOEJCQURFSEpGQ09LR05MTlNMR1BMSFNRTVhSSV1YT1ZWVFpTUVpZVVxcW1xcYWJdVWJdW2RhVWBhXWxnW2xnXXBnVHBpVmVjYmllYWxoZmxranRvZXFsa3JwanJwbXRwbnFxcXRxcXVzdHZ2c3d3dXp2dH13eHt4c3p4dXx5dX18em58snWDs36KuIF+fISBfoiEfoKCgIOEgoSCgYWFgYaFhYeHiIeIhomGg4qIhouMh4+Igo2IhYmJiYyKiY6NiY6OjY6OkpGNipCPk5KRjZiVj5KRkZOSlJaSkZSTlJWUkpWVlZiVkpiWlZyVkJuXmJqZlpmZmJucmZ2amZ2bnJycmp2dnKGenKKhnaKioaOjpKShoaWkoqampKenqKeopamlo6ioo6ippayppq6tpqmpqKurrK2qqa2tqq2traqssq+xtbGuq7CusLKxrri3rbGxsbO0sbK0tLSysraztLW1srW1tLa1uLe4tLa4uLi2tLm5tbm5ubq6vbu8uLu+v7y7ub29ub29vZaewZifwpqiw6Oqx7y9wb7Awr3C0cC+vMC/wcHBvcHBwcLBxsPEwMTCwMXEwsXFxcTFysfIxcbJzMjGw8jHysnIw8nIxs3JxsnJycnKzcvMyMjMz8zKyc7LzM3Nyc3NzcTI1MrM0s/Qzc7R1M7R2NDNzNDP1NHQzdLR0dHS1dPU0NLU19TS0NTS1dXU0tXV1dPV2dbY29jW1djX2dnZ1tnZ2dna3dvc2dvc3d3a2d3d2t3d3dbZ4dvc4d/g393g5d7h6eDe3eDf4OHg3uHh4eHi5uPk4+Pk5eTj4+Xj5eTk4uXl5eTk6efo6urn5ujn6+np6enp7evt7+zr6+zq7u3t7e3t8O7w8fDv8vHx8fLx9PX19fn5+f39/QAAAAAAACH5BAEAAP8ALAAAAAAwAEAAAAj/AP8JHEiwoMGDCBMqXDhw3a9MlRpJegaPoUWL9WzZAdIFDRoqRf48u0jyILxPUQTt68ey37YoMWyVnPkvHy4r1Vrq7Ddmx0iaF9eledVyX76VLcekiQfUIio5LFVlMHAAAQMV6/pZW7Kt6UJ7khqxzOBAwQIHAwqoYOlmkVeF+c48Yymh7NkCB06wLOT2LcJ8W3Lyq2vWAd4Uez35/VsnU798DiCYfXCgwAuWejItRthnTr96EhYYMJCgQIEb/eB1qbb5YKMd2/ppWrGHBhMVHnLayVSxdcFqXaTU67eKH51xuFZ+mvPL90FVP5b8ysUvjTtY++yMUeUcYSMzWmDx//tSL9cRN5q9/rogwSI5UsWZ5IM1J9bbRgoIZLh4D1YjEnnA4pchCgggAAFBJHQPO910Q0wwsBxCCiywBANNOzQJ4kAAAQhQAAFSGFSONdGUCI0spGRiiSakvEIhhdGQ9IwEAjggQABpIUBQOtZ0E80neqChxRhxuNEGGV2M8YYdg6xCITQWbXMDDQ4wcKMEXuTzTz0NVqNIFUeggccknqjySSaZfDIJH2QEUUMVlMBCCzkXnaAAAgjUIQwYDWIzyRFOzOGJK6mk4gkmp/SxSSqs4ILLcjaIwAQqufzEUAcXNHNEFB58k4wbUtjhSSpheWLLM8psk0g1vuCCySKePP/6BQokCJKLMUEtM8cEiNCyxRipuKJHI69s88w26wiiBh5RuLKNOsjgYkgmuOSRgwZfWEjSKt2osgQds2RiyETbkLPOuTZIoQM5X5xLDjnVTFJHsG1UQMQwy1gUDzuzCNHHLYtk8ksebtQhyLnrVMGEDZ/EgTAyjeBSTSOY+FJHBUsME6NC8bzTDBR0AHxKNeRUEQYTZ7j7yilrsFENwlwwsc4223xS8RwNwJEMnQjps++QvlySirnrYHLEJ06sg0sv5BSyhC+//HIuLltQc64zqVQcRQSqQIlQPPHoccQrnnzy7rnIvBK1xO/awUQk61TzijqnpPwwJq7gkgIJ1XT/ZRA+9lSDAiGzTEQ0wusgU8kkuGyDyhIvV3MMK4gj/AwkyGTyQB/WaFmQPfY4kYMvggiSSBq4oOENwrggQwckNRdRLi6SIHP4Ou9uU0ztR3xQbkH64JOOBXrM4gk5eKCsiyHuPoP1L9uc0YLtyNhSzcvn/mLDpqSmHQEk1nyOjx4hHIMJMtvMEsYRAZ+rDcnv/gJDCWfAv40zhwvxhxrr+NLKMzfQwTZ6IxB76KMFRzhGJ162DVU8oxEIM5Y6xmGsGnDgEtQYhziqwYyzrUMOTjgDObZxim0IYgPV6AZB9DEPCkjiFsyoHOLUgYtYREEP1ZDEDY6BjCsU4hfPeNft/2bGC2Q84wOY8FtN8vEKCjxjFrZbBzzcJURyvCIPhWhENQzxBGpsAw11MJsQZUgN69WgDeTwXD7ygQcUlHGIVYRXsbZRDSEc4RrlAmIcEUeOZzhrCVUYRz0KqI8s5IAas6DjMRLBvDj20YHUyAENqFENXASxijI8xy3IAYcmkKM39siHEIRADUxIIw/OuEEjHVkNV0iCBS6QhAMd6S4rlEES6TMHIbDwyQKKcgjVmIU0lmAETCTNkSOsRh1kMINAHGuMlfsCE+jgikSQIxFf6OWWROkDVo2DHJ/Iwx+QmcxjmAAHx/AiNBEXhkRkAR1DS4QYtEmPfFSBBtWwhTrWgf8Oco6QZtW4gRyc0TdMIo4ayJBGNmxxzTpoE2xy+EA1igE/f9KMjiqoAyXLtc7KkcMV5OiDNQe5JXqoggHP8AU1/CnEcGhDG6pAhja2EQ4PyhB3woAeHD6xDc/9Ix7rWEAjijGLvnEUmaZIglKRoAQkAGOIHvWFM5QRBugRhB30IEECz7cNNzzjCFBwJDRCMYpRiEIUoEAW4pwhh23owVwkFIck2kAzgrwjHmrAgC1qt40j1AEOVkBmN6xBDmtY4xtDTJ0TbDGHbfziFdXIwyKssQ6C1IMd02AAHVQhB0wwgQc+kALuzmbQm67jE3YQ4RnUgYln4KINvAhfQRp0AxD/YOIPraMZVPlo2kmEQX2+KIQnqlGIP6TQIOxgBzQYEAZP6OGZVFxHOhBGKkdgz7TkQIYkwoELOliPgFeNBxQokIhFLMJ2I5SCIY6ADBke7hXPYNy5yoWJYzwDEIuIBja+ltwRsAATgPgEej9RhBtIDXc39cMWVHCK0WJiFs+4RB+SEQ2SHqQc7MjFBpiAiULEFBljkAIaDoY7QZBDEM9A2BhsgYnAPsMTragGaq2nRIR8gx2ZQAEXMCEJSVCjEOC0XB56kAgtnAsdS2gEFCbxi0vU4sV5UEU0WLOQe3wDHJCoQRQK1YbWeRAZdbiBM7qAuG3wIhO1QwYm+nAKaHiN3yH26MY3VLGDG8wha5L4Bfq2gQwrqKIKnkCWF13libQt4g+ucLOF4dygZEThBl2g1qEkgYtfVGMR2njGM3SBCVMh4xOL0AMhcOFme9CkQd1Y7xLa0AhVsLgQmAgEIxDlCmTAQlx6yEMnkuFmr5SjG9yAhiCYcIQuyIEPjXjEJBYhCUwYQg9zcAMdKIGvZZRjMcAOdinu8AUngHUKWqiCFKQwBj2QYhhulu1m7PENNz9oF6WYxCMaYQlOUEgWw0iGCrsjkHh0YxjByMUuZLGLXMhCFrmIBlP4jRB5gM0vAQEAOw=="@ ## 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 windowfunction 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