Track Session Events

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.

Introduction

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.

Discussion

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

The Code

Using ISensLogon

isenslogon.py

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')

isenslogon_service.py

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)

With Extended Service Notifications

session_service.py

# _*_ 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)