NB: All the code and investigation behind this was done by Klaas Tjebbes ("le dahut") on the python-win32 list. All I've done is to frame it with some explanatory text and reduce the code very slightly to illustrate the key points.
The requirement: to track session events. This includes locking and unlocking the session, logon and logoff, the screen saver cutting in and cutting out, and the shell startup.
The first technique here is to use the SENS subsystem which monitors system events such as network, logon and power/battery events. The code below makes use of the ISensLogon interface, which is an event sink. It runs as a service and will receive events when a user logs on or off, when the screen is locked or unlocked, when the screensaver cuts in or out, and when the shell starts up.
The second technique uses the extended handler functionality of the service manager, available only from WinXP onwards. This offers a variety of possible notifications, including those for logon / logoff. The code then queries the Local Security database to determine the session being changed.
For illustration purposes, the code below simply logs these events to the Application event log, but the full code from Klaas includes extra functionality to log on and execute code as a particular user.
Klaas notes: We use it in a Samba Domain to configure workstations at user logon. At logon event the service reads a configuration stored on the PDC and get some rules to apply depending on: the computer name; the user name and/or groups
In addition to providing the functionality discussed above, this code also provides a working example of using pythoncom to provide an arbitrary COM server which is also an event sink. The GUIDs, as the code notes, have to be copied manually from a header file. The use of the DesignatedWrapPolicy as the superclass (and the corresponding call to self._wrap_(self) in the __init__ method) indicate to the pythoncom mechanism that the list of attributes will be specified in the _public_methods_ class attribute. After that, it's simply a question of implementing the methods (and they must all be implemented even if they're doing nothing).
# _*_ coding: iso-8859-1 _*_ import servicemanager import win32com.client import win32com.server.policy import pythoncom ## from Sens.h SENSGUID_PUBLISHER = "{5fee1bd6-5b9b-11d1-8dd2-00aa004abd5e}" SENSGUID_EVENTCLASS_LOGON = "{d5978630-5b9f-11d1-8dd2-00aa004abd5e}" ## from EventSys.h PROGID_EventSystem = "EventSystem.EventSystem" PROGID_EventSubscription = "EventSystem.EventSubscription" IID_ISensLogon = "{d597bab3-5b9f-11d1-8dd2-00aa004abd5e}" class SensLogon(win32com.server.policy.DesignatedWrapPolicy): _com_interfaces_=[IID_ISensLogon] _public_methods_=[ 'Logon', 'Logoff', 'StartShell', 'DisplayLock', 'DisplayUnlock', 'StartScreenSaver', 'StopScreenSaver' ] def __init__(self): self._wrap_(self) def Logon(self, *args): logevent('Logon : %s'%[args]) def Logoff(self, *args): logevent('Logoff : %s'%[args]) def StartShell(self, *args): logevent('StartShell : %s'%[args]) def DisplayLock(self, *args): logevent('DisplayLock : %s'%[args]) def DisplayUnlock(self, *args): logevent('DisplayUnlock : %s'%[args]) def StartScreenSaver(self, *args): logevent('StartScreenSaver : %s'%[args]) def StopScreenSaver(self, *args): logevent('StopScreenSaver : %s'%[args]) def logevent(msg, evtid=0xF000): """log into windows event manager """ servicemanager.LogMsg( servicemanager.EVENTLOG_INFORMATION_TYPE, evtid, # generic message (msg, '') ) def register(): logevent('Registring ISensLogon') sl=SensLogon() subscription_interface=pythoncom.WrapObject(sl) event_system=win32com.client.Dispatch(PROGID_EventSystem) event_subscription=win32com.client.Dispatch(PROGID_EventSubscription) event_subscription.EventClassID=SENSGUID_EVENTCLASS_LOGON event_subscription.PublisherID=SENSGUID_PUBLISHER event_subscription.SubscriptionName='Python subscription' event_subscription.SubscriberInterface=subscription_interface event_system.Store(PROGID_EventSubscription, event_subscription) pythoncom.PumpMessages() logevent('ISensLogon stopped')
This code is a fairly standard Python Windows service wrapper which starts the ISensLogon COM Server as part of its SvcDoRun method. The COM Server then runs a message pump until the SvcStop sends it a quit message. You can run this service in debug mode but this obviously won't survive a logoff/logon session change.
# _*_ coding: iso-8859-1 _*_ # this example has been made with the help of Roger Upole # # this method *IS* Windows 2000 compatible # # for details about ISensLogon # see http://msdn.microsoft.com/en-us/library/aa376860(VS.85).aspx import win32serviceutil import win32service import win32api import servicemanager # the "magic" import import isenslogon svcdeps=["EventLog"] class ISensLogonService(win32serviceutil.ServiceFramework): """ Definition du service Windows """ _svc_name_ = _svc_display_name_ = 'ISensLogon service' _svc_deps_ = svcdeps def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) isenslogon.logevent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTING) self.ReportServiceStatus(win32service.SERVICE_START_PENDING, waitHint=30000) def SvcDoRun(self): isenslogon.logevent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTED) isenslogon.register() self.ReportServiceStatus(win32service.SERVICE_STOPPED) def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) # this does not seem to be the best way to stop PumpMessages() # it looks like it stops the whole service win32api.PostQuitMessage() # I think that's why "stop" events are not logged into Windows Event Manager self.ReportServiceStatus(win32service.SERVICE_STOPPED) # but it works... # one can also use win32api.PostThreadMessage # therefor, get the thread ID and call # win32api.PostThreadMessage(isenslogon_thread_id, 18) #reboot/halt make a different call than 'net stop mytestservice' SvcShutdown = SvcStop if __name__ == '__main__': win32serviceutil.HandleCommandLine(ISensLogonService)
# _*_ coding: iso-8859-1 _*_ # This example is based on "win32/Demos/service/serviceEvents.py" # The session event dectection is *NOT* Windows 2000 compatible # it is only available since Windows XP # see http://msdn.microsoft.com/en-us/library/ms683241(VS.85).aspx import win32serviceutil import win32service import win32event import win32ts import servicemanager import win32security import win32process import win32profile import win32con svcdeps=["EventLog"] class SessionService(win32serviceutil.ServiceFramework): """ Definition du service Windows """ _svc_name_ = 'MyTestServ' _svc_display_name_ = 'My test service (long name)' _svc_deps_ = svcdeps def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) logevent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTING) self.ReportServiceStatus(win32service.SERVICE_START_PENDING, waitHint=30000) self.stop_event = win32event.CreateEvent(None, 0, 0, None) def GetAcceptedControls(self): # Accept SESSION_CHANGE control rc = win32serviceutil.ServiceFramework.GetAcceptedControls(self) rc |= win32service.SERVICE_ACCEPT_SESSIONCHANGE return rc # All extra events are sent via SvcOtherEx (SvcOther remains as a # function taking only the first args for backwards compat) def SvcOtherEx(self, control, event_type, data): # This is only showing a few of the extra events - see the MSDN # docs for "HandlerEx callback" for more info. if control == win32service.SERVICE_CONTROL_SESSIONCHANGE: sess_id = data[0] if event_type == 5: # logon msg = "Logon event: type=%s, sessionid=%s\n" % (event_type, sess_id) user_token = win32ts.WTSQueryUserToken(int(sess_id)) elif event_type == 6: # logoff msg = "Logoff event: type=%s, sessionid=%s\n" % (event_type, sess_id) else: msg = "Other session event: type=%s, sessionid=%s\n" % (event_type, sess_id) try: for key, val in self.GetUserInfo(sess_id).items(): msg += '%s : %s\n'%(key, val) except Exception, e: msg += '%s'%e logevent(msg) def GetUserInfo(self, sess_id): sessions = win32security.LsaEnumerateLogonSessions()[:-5] for sn in sessions: sn_info = win32security.LsaGetLogonSessionData(sn) if sn_info['Session'] == sess_id: return sn_info def SvcDoRun(self): logevent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTED) win32event.WaitForSingleObject(self.stop_event, win32event.INFINITE) self.ReportServiceStatus(win32service.SERVICE_STOPPED) def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) win32event.SetEvent(self.stop_event) #reboot/halt make a different call than 'net stop mytestservice' SvcShutdown = SvcStop def logevent(msg, evtid=0xF000): """log into windows event manager """ servicemanager.LogMsg( servicemanager.EVENTLOG_INFORMATION_TYPE, evtid, # generic message (msg, '') ) if __name__ == '__main__': win32serviceutil.HandleCommandLine(SessionService)