PowerShell : Recording Process Run Time (Or, How Long Have I Played That Game For?)

I did a post about all the games I played in 2014.  The only source of data I had was the total play-time from Steam, but I really wanted a way of getting more detailed information (like when I played them and how long for).

I thought about it and I what I wanted was script that would record the time processes that were running on my system and write them to a file.  The script would stay running and update the file at periodic intervals to record how long the processes  had been running for.

Ideally the recorded processes would be broken down by the date so that I can see what I’ve run on a daily basis.  I could also use that date and the name of the process to match any processes that have already been recorded in the file and update their run-time when the script cycles.

Finally, I don’t want to record all the processes I’m running so I need a way to filter what is saved.

Here’s the script;  it’s generic so it will keep a running tally of any processes that match a certain pattern in the path of their executable;  to monitor my games I’d check “Games” was in the path somewhere, but it could just as easily be “Sophos” or “Windows” if you want to record those processes.

[CmdletBinding()]
param
(
    [string]$SourceFolder="*games*",
    [string]$DataFilePath="d:\users\neil\Documents\ProcessTimes.csv"
)
$DefaultDataFileName="ProcessTime.csv"
if ($DataFilePath -eq "")
{
    $DataFilePath=$PSScriptRoot + "\" +$DefaultDataFileName
}
function Get-MatchingProcess
{
    param
    (
        [System.Array]$DataArray,
        [parameter(Mandatory=$True)]
        [System.Object]$Item
    )
    $DataArray | ? {($_.Path -eq $Item.Path) -and (
        (get-date -date $_.Start -Millisecond 0) -eq (
        get-date -date $Item.Start -Millisecond 0))} |
            Select-Object -First 1
}
Write-Verbose "Process Search String : $SourceFolder"
Write-Verbose "Data File Location : $DataFilePath"
while($true)
{
    Write-Verbose ("Running cycle.")
    if (Test-Path $DataFilePath)
    {
        [System.Array]$Data=Import-Csv -Delimiter ',' -LiteralPath $DataFilePath
    }else
    {
        $Data=@()
    }
    $FilteredProcesses=get-process | ? {$_.Path -like $SourceFolder} | select Path,@{
        name="Start";expression={$_.StartTime}},@{name="RunningTimeInSeconds";
        expression={(New-TimeSpan -Start ($_.StartTime)).TotalSeconds}}
    Foreach ($Process in $FilteredProcesses)
    {
        $Result=Get-MatchingProcess $Data $Process
        if ($Result)
        {
            Write-Verbose "Modifying Existing Record."
            $Result.RunningTimeInSeconds=(New-TimeSpan -Start $Process.Start).TotalSeconds
        }else
        {
            Write-Verbose "Adding New Record."
            $Data=$Data+$Process
        }
    }
    $Data | Export-Csv -Path $DataFilePath -NoTypeInformation
    sleep 20
}

I run this automatically when I log on via scheduled tasks and it maintains a log of every process that runs that has “games” in the executable path.  It updates every 20 seconds.

It’s looking like it’s working pretty well so here’s a breakdown of how it works:

[CmdletBinding()]
param
(
    [string]$SourceFolder="*games*",
    [string]$DataFilePath="d:\users\neil\Documents\ProcessTimes.csv"
)
$DefaultDataFileName="ProcessTime.csv"
if ($DataFilePath -eq "")
{
    $DataFilePath=$PSScriptRoot + "\" +$DefaultDataFileName
}

A fairly standard start.  I put [CmdletBinding()] at the beginning so I can use Write-Verbose.

The only two parameters I want are what to filter the processes by ($SourceFolder, set to “games” by default)  and where to write the log file to ($DataFilePath).  If I don’t specify this last parameter it sets the filename for the log to ProcessTime.csv and writes it to the same directory the script is in (via setting $DataFilePath).


function Get-MatchingProcess
{
    param
    (
        [System.Array]$DataArray,
        [parameter(Mandatory=$True)]
        [System.Object]$Item
    )
    $DataArray | ? {($_.Path -eq $Item.Path) -and (
        (get-date -date $_.Start -Millisecond 0) -eq (
        get-date -date $Item.Start -Millisecond 0))} |
            Select-Object -First 1
}
Write-Verbose "Process Search String : $SourceFolder"
Write-Verbose "Data File Location : $DataFilePath"

The script is going to run constantly and update the file with running processes every 20 seconds.  I don’t want it to add new entries every time it runs (as it’ll make the file huge).  Instead I want it to read the file and update any entries that already exist for today’s date, or write a new entry if they don’t exist.

I know that I’m going to record the process path, time running and the process start date/time.  That way I can check if there is already an entry for the process that I’m about to record.

The Get-MatchingProcess function looks in a collection of process details (this will be the contents of the log file I’ve read in) and sees if the process I’m checking exists; if it does then I return it.

The comparison itself is interesting;


    $DataArray | ? {($_.Path -eq $Item.Path) -and (
        (get-date -date $_.Start -Millisecond 0) -eq (
        get-date -date $Item.Start -Millisecond 0))} |
            Select-Object -First 1

Why not a straight comparison between the Start date of current record against the Start date of the process we’re examining at the moment (held in $Item)? When I tried that though, it didn’t work. If the two variables were identical, they didn’t match.

It took a bit of figuring out; when I examined the contents of both of the variables they were the same, down to the second. I first blamed type conversions (as the Start property from $Item was a [DateTime] and the one from the file on the disk was a string). No dice.

Next came all sorts of jumping through hoops with no more joy. Eventually I found a small post about how [DateTime] objects are actually compared using their Ticks property. There I saw that the two variables differed.

The explanation was that when the [DateTime] is written to a file, it only writes it with a precision down to the seconds level, but a [DateTime] variable normally includes precision down to the millisecond level. So the milliseconds were stripped off when I wrote the process start time to disk (and read it back in) which meant it was different to the start time of the actual process itself (which still had the milliseconds value on it).

The statement sets both of the milliseconds to 0 before the comparison and that resolves the issue.

The final statements use Write-Verbose to log what the script is doing; I can run it with the -Verbose switch if I want to see what’s going on.


while($true)
{
    Write-Verbose ("Running cycle.")
    if (Test-Path $DataFilePath)
    {
        [System.Array]$Data=Import-Csv -Delimiter ',' -LiteralPath $DataFilePath
    }else
    {
        $Data=@()
    }
    $FilteredProcesses=get-process | ? {$_.Path -like $SourceFolder} | select Path,@{
        name="Start";expression={$_.StartTime}},@{name="RunningTimeInSeconds";
        expression={(New-TimeSpan -Start ($_.StartTime)).TotalSeconds}}
    Foreach ($Process in $FilteredProcesses)
    {
        $Result=Get-MatchingProcess $Data $Process
        if ($Result)
        {
            Write-Verbose "Modifying Existing Record."
            $Result.RunningTimeInSeconds=(New-TimeSpan -Start $Process.Start).TotalSeconds
        }else
        {
            Write-Verbose "Adding New Record."
            $Data=$Data+$Process
        }
    }
    $Data | Export-Csv -Path $DataFilePath -NoTypeInformation
    sleep 20
}

This is the meat of the script. It loops forever (while ($true)) with a pause of 20 seconds between each iteration (sleep 20, at the end of the while loop).

The script reads all the existing records from the CSV file into $Data; if the file doesn’t exist it creates a new, empty array.

Next $FilteredProcesses is created. This is an array of custom objects the script gets from all the processes running on the system. It initially filters them so it only gets processes that match the path we want (? {$.Path -like $SourceFolder}) and for each matching process it creates a custom object that includes the process executable path, its start time and the time the process has been running in seconds (I use New-TimeSpan to measure the difference between the StartTime property of the process and the current time; {(New-TimeSpan -Start ($.StartTime)).TotalSeconds}).

It then loops through all the processes in the $FilteredProcesses array and see if each one exists in $Data (using the Get-MatchingProcess function defined earlier in the script). If it does the original record is updated with the new running time. If it doesn’t, a new record is added with the Path, StartTime and RunningTimeInSeconds information.

When all the processes in $FilteredProcesses are checked, all the records in $Data are written to the CSV file, ready to be read back in when the process loops again.

I set the script to run automatically when I log in using Scheduled Tasks and it’s been running great!

UPDATE : I’ve made some improvements to the script;  see here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: