The time comes when a manager walks into your office and says "We're tightening the belt this year. One way we're looking to save money is to remove unused PCs from 1 area and move it into another area. Give me a list of all PCs that haven't been used in X days." You agree to do it without thinking as usual and think no big deal. After a few minutes of looking into it you then think "...ummm, what really does "unused" mean anyway?" Does "unused" mean users aren't working on it and just use it as a Facebook machine? Does "unused" mean X application isn't being executed on it or does it simply mean no keyboard or mouse activity at all?

After you grill the manager a little bit you find out that he wants all PCs that haven't had any kind of keyboard or mouse activity for 1 week. Fine. Game on.

I came across this awesome StackOverflow question which did 90% of the work for me.

Add-Type @'
using System; using System.Diagnostics;
using System.Runtime.InteropServices;namespace
PInvoke.Win32 {
    public static class UserInput {
        [DllImport("user32.dll", SetLastError=false)]
        
        private static extern bool GetLastInputInfo(
            ref LASTINPUTINFO plii
        );
        [StructLayout(LayoutKind.Sequential)]
        
        private struct LASTINPUTINFO {
            public uint cbSize;
            public int dwTime;
        }
        
        public static DateTime LastInput {
            get {
                DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
                DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
                return lastInput;
            }
        }
        
        public static TimeSpan IdleTime {
            get {
                return DateTime.UtcNow.Subtract(LastInput);
            }
        }
        
        public static int LastInputTicks {
            get {
                LASTINPUTINFO lii = new LASTINPUTINFO();
                lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
                GetLastInputInfo(ref lii);
                return lii.dwTime;
            }
        }
    }
}
'@"

$(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes)" | Add-Content -Path "\\SERVER\C$\FOLDER\$($env:COMPUTERNAME)-idletime.txt"

This is nearly exactly the same as the author example but I needed to modify it for a mass deployment. All I'm doing it executing the script on a workstation and getting the total idle time in minutes.

I'm then adding this to a file called PCNAME-idletime.txt on a server. I'm using SCCM so the deployment of the script was trivial.

After all workstations completed and I had a whole bunch of text files in the server's folder I then merged them all into 1 single CSV and voila!, the manager got what he wanted!

Get-ChildItem '\\SERVER\C$\FOLDER' | select @{n='PCName';e={$_.BaseName.Replace('-idletime.txt','')}},@{n='IdleTime';e={(Get-Content $_.FullName)}} | Export-Csv -NoTypeInformation -Append -Path CSVFILE.csv

This one-liner lists all the text files and using calculated properties creates a name to idle time reference which I then export out into a CSV. What I end up with is something like this:

PCName,Idletime PC1,345 PC2,123 PC3,0

I hope this helps the next time you want to get unused PCs!

Join the Jar Tippers on Patreon

It takes a lot of time to write detailed blog posts like this one. In a single-income family, this blog is one way I depend on to keep the lights on. I'd be eternally grateful if you could become a Patreon patron today!

Become a Patron!