Search code examples
pythonwindowsserviceuser-inputuser-inactivity

How can I check for user activity/idle from a Windows service written in Python?


I've written a Windows service in Python that needs to be able to detect user activity. Under normal circumstances I would call the Windows GetLastInputInfo method, but that method doesn't work when called by a service. Here's the relevant info from the documentation for this method:

This function is useful for input idle detection. However, GetLastInputInfo does not provide system-wide user input information across all running sessions. Rather, GetLastInputInfo provides session-specific user input information for only the session that invoked the function.

The salient point is this: "for only the session that invoked the function"

If called by the service, GetLastInputInfo will always return 0 because the service is running in session 0 and doesn't receive input! How can my service detect user activity from the console session?


Solution

  • Caveats Up Front:

    This solution is tested and working under Windows 10.

    I was able to do some limited testing of this solution under Windows 11, and it seems like mouse activity (button presses and motion) is not registered, but key press and key release events are. If all you need is key activity detection, this will work. But if you need to monitor mouse events, you're out of luck pending a different/updated solution.

    If anyone has an alternative solution for Windows 11, I'm all ears!


    Fortunately, there's a workaround for this problem! While you can't poll for user activity directly from a service, you can check to see if the system is currently handling user input by querying the I/O info for the Windows Client Server Runtime process (a.k.a. "csrss.exe").

    By leveraging Python's psutil module, you can check either the read_count or read_bytes properties of csrss.exe. These values should change any time there is input from the user, i.e. keystrokes or mouse events.

    First, you need to get the process ID (PID) for the csrss.exe process:

    import psutil
    
    
    def get_csrss_pids() -> list[int]:
        """Get the PID(s) for the Windows Client Server Runtime process"""
        # NOTE: There will likely be two csrss processes running; one in
        # the Services session (0), and the other in the Console session (1)
        return [
            proc.pid for proc in psutil.process_iter(attrs=['name'])
            if proc.name() == 'csrss.exe'
        ]
    

    Once you have your csrss.exe PID(s), you can use psutil's io_counters method to get the read_count or read_bytes info (both will update any time there's user input - I use read_bytes below)

    def get_io(pids: list[int]) -> list[int]:
        """Returns the last `read_bytes` value for the given csrss.exe PID(s)"""
        # NOTE: if multiple PIDs are given, it's likely that only one of the PIDs
        # 'read_bytes' values will be changing on user input because one of these
        # processes is for your current session and the others aren't
        return [psutil.Process(pid).io_counters().read_bytes for pid in pids]
    

    The get_io function will return a list of integers corresponding to the read_bytes values for each of the given csrss.exe process IDs. To check for user activity, this list should be periodically compared to a previously stored value - any changes mean there's been input from the user!

    Here's a quick demo:

    import psutil
    
    
    def get_csrss_pids() -> list[int]:
        """Get the PID(s) for the Windows Client Server Runtime process"""
        return [
            proc.pid for proc in psutil.process_iter(attrs=['name'])
            if proc.name() == 'csrss.exe'
        ]
    
    
    def get_io(pids: list[int]) -> list[int]:
        """Returns the last `read_bytes` value for the given csrss.exe PID(s)"""
        return [psutil.Process(pid).io_counters().read_bytes for pid in pids]
    
    
    pids = get_csrss_pids()
    last_io = get_io(pids)  # store an initial value to compare against
    
    while True:
        try:
            if (tick := get_io(pids)) != last_io:  # if activity is detected...
                print(tick)  # do something
                last_io = tick  # store the new value to compare against
        except KeyboardInterrupt:
            break
    

    To incorporate these functions into your service, simply include them in your main class (the one subclassing ServiceFramework) - don't forget to add the self parameter!

    You'll want to call get_csrss_pids and set the initial value of last_io at __init__ and go from there:

    class MyService(ServiceFramework):
        _svc_name_ = 'my_service'
        _svc_display_name_ = 'My Service'
    
        def __init__(self, *args):
            super().__init__(*args)
            self.csrss_pids = self.get_csrss_pids()
            self.last_io = self.get_io()
            ...