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 the read_bytes
property of csrss.exe. This value 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: more than one instance of csrss.exe may be running on your
# machine, so you'll want to gather all of the matching PIDs!
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_bytes
info
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()
...