web-dev-qa-db-fra.com

Être averti de la connexion et de la déconnexion

Je dois développer un programme qui circule sur un PC local en tant que service, un service de livraison de l'état de l'utilisateur à un serveur. Au début, je dois détecter l'utilisateur connexion et logoff.

Mon idée était d'utiliser la classe ManagementEventWatcher et d'interroger le Win32_LogonSession Pour être notifié si quelque chose a changé.

Mon premier test fonctionne bien, voici la partie de code (cela serait exécuté en tant que fil d'un service) :

private readonly static WqlEventQuery qLgi = new WqlEventQuery("__InstanceCreationEvent", new TimeSpan(0, 0, 1), "TargetInstance ISA \"Win32_LogonSession\"");

public EventWatcherUser() {
}

public void DoWork() {
    ManagementEventWatcher eLgiWatcher = new ManagementEventWatcher(EventWatcherUser.qLgi);
    eLgiWatcher.EventArrived += new EventArrivedEventHandler(HandleEvent);
    eLgiWatcher.Start();
}

private void HandleEvent(object sender, EventArrivedEventArgs e)
{
    ManagementBaseObject f = (ManagementBaseObject)e.NewEvent["TargetInstance"];
    using (StreamWriter fs = new StreamWriter("C:\\status.log", true))
    {
        fs.WriteLine(f.Properties["LogonId"].Value);
    }
}

Mais j'ai des problèmes de compréhension et je ne sais pas si c'est le moyen courant de résoudre cette tâche.

  1. Si je demande Win32_LogonSession Je reçois plusieurs enregistrements associés au même utilisateur. Par exemple, j'obtiens cet IDS 7580798 et 7580829 et si j'existait

    Associatifs de {win32_logonsession.logonid = x} Où résultatclass = win32_userAccount

    Je reçois le même enregistrement pour différents identifiants. (Win32_USERAccount.domain = "Nom PC", Nom = "User1")

    Pourquoi existe-t-il plusieurs session de connexion avec le même utilisateur? Quel est le moyen courant d'obtenir le courant connecté à l'utilisateur? Ou mieux Comment être notifié correctement par la connexion d'un utilisateur?

  2. Je pensais pouvoir utiliser de la même manière avec __InstanceDeletionEvent Pour déterminer si un utilisateur se déconnecte. Mais je suppose que si l'événement est élevé, je ne peux pas interroger Win32_UserAccount Pour le nom d'utilisateur après cela. J'ai raison?

Je suis à la bonne direction ou y a-t-il de meilleures façons? Ce serait génial si tu pouvais m'aider!

éditer est la classe WTSregistersSessionNotification de la bonne voie? Je ne sais pas si c'est possible, car dans un service, je n'ai pas de gestionnaire de fenêtre.

22
Andre Hofmeister

J'utilise Servicebase.onsessionChanger pour attraper les différents événements utilisateur et charger les informations nécessaires par la suite.

protected override void OnSessionChange(SessionChangeDescription desc)
{
    var user = Session.Get(desc.SessionId);
}

Pour charger les informations de session, j'utilise le wts_info_class . Voir mon exemple ci-dessous:

internal static class NativeMethods
{
    public enum WTS_INFO_CLASS
    {
        WTSInitialProgram,
        WTSApplicationName,
        WTSWorkingDirectory,
        WTSOEMId,
        WTSSessionId,
        WTSUserName,
        WTSWinStationName,
        WTSDomainName,
        WTSConnectState,
        WTSClientBuildNumber,
        WTSClientName,
        WTSClientDirectory,
        WTSClientProductId,
        WTSClientHardwareId,
        WTSClientAddress,
        WTSClientDisplay,
        WTSClientProtocolType,
        WTSIdleTime,
        WTSLogonTime,
        WTSIncomingBytes,
        WTSOutgoingBytes,
        WTSIncomingFrames,
        WTSOutgoingFrames,
        WTSClientInfo,
        WTSSessionInfo
    }

    [DllImport("Kernel32.dll")]
    public static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    public static extern bool WTSQuerySessionInformation(IntPtr hServer, Int32 sessionId, WTS_INFO_CLASS wtsInfoClass, out IntPtr ppBuffer, out Int32 pBytesReturned);

    [DllImport("Wtsapi32.dll")]
    public static extern void WTSFreeMemory(IntPtr pointer);
}

public static class Status
{
    public static Byte Online
    {
        get { return 0x0; }
    }

    public static Byte Offline
    {
        get { return 0x1; }
    }

    public static Byte SignedIn
    {
        get { return 0x2; }
    }

    public static Byte SignedOff
    {
        get { return 0x3; }
    }
}

public static class Session
{
    private static readonly Dictionary<Int32, User> User = new Dictionary<Int32, User>();

    public static bool Add(Int32 sessionId)
    {
        IntPtr buffer;
        int length;

        var name = String.Empty;
        var domain = String.Empty;

        if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTS_INFO_CLASS.WTSUserName, out buffer, out length) && length > 1)
        {
            name = Marshal.PtrToStringAnsi(buffer);
            NativeMethods.WTSFreeMemory(buffer);
            if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTS_INFO_CLASS.WTSDomainName, out buffer, out length) && length > 1)
            {
                domain = Marshal.PtrToStringAnsi(buffer);
                NativeMethods.WTSFreeMemory(buffer);
            }
        }

        if (name == null || name.Length <= 0)
        {
            return false;
        }

        User.Add(sessionId, new User(name, domain));

        return true;
    }

    public static bool Remove(Int32 sessionId)
    {
        return User.Remove(sessionId);
    }

    public static User Get(Int32 sessionId)
    {
        if (User.ContainsKey(sessionId))
        {
            return User[sessionId];
        }

        return Add(sessionId) ? Get(sessionId) : null;
    }

    public static UInt32 GetActiveConsoleSessionId()
    {
        return NativeMethods.WTSGetActiveConsoleSessionId();
    }
}

public class AvailabilityChangedEventArgs : EventArgs
{
    public bool Available { get; set; }

    public AvailabilityChangedEventArgs(bool isAvailable)
    {
        Available = isAvailable;
    }
}

public class User
{
    private readonly String _name;

    private readonly String _domain;

    private readonly bool _isDomainUser;

    private bool _signedIn;

    public static EventHandler<AvailabilityChangedEventArgs> AvailabilityChanged;

    public User(String name, String domain)
    {
        _name = name;
        _domain = domain;

        if (domain.Equals("EXAMPLE.COM"))
        {
            _isDomainUser = true;
        }
        else
        {
            _isDomainUser = false;
        }
    }

    public String Name
    {
        get { return _name; }
    }

    public String Domain
    {
        get { return _domain; }
    }

    public bool IsDomainUser
    {
        get { return _isDomainUser; }
    }

    public bool IsSignedIn
    {
        get { return _signedIn; }
        set
        {
            if (_signedIn == value) return;

            _signedIn = value;

            OnAvailabilityChanged(this, new AvailabilityChangedEventArgs(IsSignedIn));
        }
    }

    protected void OnAvailabilityChanged(object sender, AvailabilityChangedEventArgs e)
    {
        if (AvailabilityChanged != null)
        {
            AvailabilityChanged(this, e);
        }
    }
}

Le code suivant utilise l'événement statique AvailabilityChanged de User, qui est tiré dès que l'état de la session change. L'arg e contient l'utilisateur spécifique.

public Main()
{
  User.AvailabilityChanged += UserAvailabilityChanged;
}

private static void UserAvailabilityChanged(object sender, AvailabilityChangedEventArgs e)
{
  var user = sender as User;

  if (user == null) return;

  System.Diagnostics.Debug.WriteLine(user.IsSignedIn);
}
1
Andre Hofmeister

Depuis que vous êtes sur un service, vous pouvez obtenir des événements de changement de session directement.

Vous pouvez vous inscrire pour recevoir le SERVICE_CONTROL_SESSIONCHANGE un événement. En particulier, vous voudrez chercher le WTS_SESSION_LOGON et WTS_SESSION_LOGOFF les raisons.

Pour plus de détails et des liens vers les Documents MSDN, Check Cette réponse que j'ai écrite hier .

En C #, il est encore plus facile, car ServiceBase enveloppe déjà la routine de contrôle de service et expose l'événement sous la forme d'une méthode OnSessionChange à vous. Voir MSDN DOCS pour ServiceBase , et n'oubliez pas de définir la propriété CanHandleSessionChangeEvent sur true pour activer l'exécution de cette méthode.

Ce que vous récupérez lorsque le framework appelle votre OnSessionChange le remplacement est une Structure de sessionChangeDescription avec une raison (déconnexion, connexion, ...) et un identifiant de session que vous pouvez utiliser pour obtenir des informations, Par exemple, sur la journalisation de l'utilisateur activé/désactivé (voir le lien à ma réponse précédente pour plus de détails)

Modifier: Exemple de code

 public class SimpleService : ServiceBase {
    ...
    public SimpleService()
    {
        CanPauseAndContinue = true;
        CanHandleSessionChangeEvent = true;
        ServiceName = "SimpleService";
    }

    protected override void OnSessionChange(SessionChangeDescription changeDescription)
    {
        EventLog.WriteEntry("SimpleService.OnSessionChange", DateTime.Now.ToLongTimeString() +
            " - Session change notice received: " +
            changeDescription.Reason.ToString() + "  Session ID: " + 
            changeDescription.SessionId.ToString());


        switch (changeDescription.Reason)
        {
            case SessionChangeReason.SessionLogon:
                EventLog.WriteEntry("SimpleService.OnSessionChange: Logon");
                break;

            case SessionChangeReason.SessionLogoff:       
                EventLog.WriteEntry("SimpleService.OnSessionChange Logoff"); 
                break;
           ...
        }
18

Vous pouvez utiliser le Service de notification de l'événement système Technologie faisant partie de Windows. Il a la interface isenslogon2 qui fournit des événements de connexion/déconnexion (et d'autres événements tels que les connexions de session à distance).

Voici un élément de code (une application d'exemple de console) qui démontre comment le faire. Vous pouvez le tester à l'aide d'une session de bureau à distance à partir d'un autre ordinateur par exemple, cela déclenchera les événements SessionDisconnect, SessionReconnect, par exemple.

Ce code doit prendre en charge toutes les versions de Windows de XP à Windows 8.

Ajouter une référence au composant COM nommé, com + 1.0 Library aka comadmin.

Remarque Assurez-vous de définir les types Interop intégrés à "Faux", sinon vous obtiendrez l'erreur suivante: "Le type interopt 'ComadminCatalClass' ne peut pas être intégré. Utilisez l'interface applicable à la place. "

Contrairement à d'autres articles que vous trouverez sur Internet sur l'utilisation de cette technologie dans .NET, il ne fait pas référence au sens.dll car ... il ne semble pas exister sur Windows 8 (je ne sais pas pourquoi). Cependant, la technologie semble prise en charge et le service SENS est en effet installé et fonctionne bien sur Windows 8, de sorte que vous devez simplement avoir besoin de déclarer manuellement les interfaces et les GUID manuellement (comme dans cet exemple) ou référence à un assemblage interopé créé sur une version antérieure de Windows. (Cela devrait fonctionner correctement lorsque les GUID et diverses interfaces n'ont pas changé).

class Program
{
    static SensEvents SensEvents { get; set; }

    static void Main(string[] args)
    {
        SensEvents = new SensEvents();
        SensEvents.LogonEvent += OnSensLogonEvent;
        Console.WriteLine("Waiting for events. Press [ENTER] to stop.");
        Console.ReadLine();
    }

    static void OnSensLogonEvent(object sender, SensLogonEventArgs e)
    {
        Console.WriteLine("Type:" + e.Type + ", UserName:" + e.UserName + ", SessionId:" + e.SessionId);
    }
}

public sealed class SensEvents
{
    private static readonly Guid SENSGUID_EVENTCLASS_LOGON2 = new Guid("d5978650-5b9f-11d1-8dd2-00aa004abd5e");
    private Sink _sink;

    public event EventHandler<SensLogonEventArgs> LogonEvent;

    public SensEvents()
    {
        _sink = new Sink(this);
        COMAdminCatalogClass catalog = new COMAdminCatalogClass(); // need a reference to COMAdmin

        // we just need a transient subscription, for the lifetime of our application
        ICatalogCollection subscriptions = (ICatalogCollection)catalog.GetCollection("TransientSubscriptions");

        ICatalogObject subscription = (ICatalogObject)subscriptions.Add();
        subscription.set_Value("EventCLSID", SENSGUID_EVENTCLASS_LOGON2.ToString("B"));
        subscription.set_Value("SubscriberInterface", _sink);
        // NOTE: we don't specify a method name, so all methods may be called
        subscriptions.SaveChanges();
    }

    private void OnLogonEvent(SensLogonEventType type, string bstrUserName, uint dwSessionId)
    {
        EventHandler<SensLogonEventArgs> handler = LogonEvent;
        if (handler != null)
        {
            handler(this, new SensLogonEventArgs(type, bstrUserName, dwSessionId));
        }
    }

    private class Sink : ISensLogon2
    {
        private SensEvents _events;

        public Sink(SensEvents events)
        {
            _events = events;
        }

        public void Logon(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.Logon, bstrUserName, dwSessionId);
        }

        public void Logoff(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.Logoff, bstrUserName, dwSessionId);
        }

        public void SessionDisconnect(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.SessionDisconnect, bstrUserName, dwSessionId);
        }

        public void SessionReconnect(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.SessionReconnect, bstrUserName, dwSessionId);
        }

        public void PostShell(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.PostShell, bstrUserName, dwSessionId);
        }
    }

    [ComImport, Guid("D597BAB4-5B9F-11D1-8DD2-00AA004ABD5E")]
    private interface ISensLogon2
    {
        void Logon([MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void Logoff([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void SessionDisconnect([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void SessionReconnect([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void PostShell([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
    }
}

public class SensLogonEventArgs : EventArgs
{
    public SensLogonEventArgs(SensLogonEventType type, string userName, uint sessionId)
    {
        Type = type;
        UserName = userName;
        SessionId = sessionId;
    }

    public string UserName { get; private set; }
    public uint SessionId { get; private set; }
    public SensLogonEventType Type { get; private set; }
}

public enum SensLogonEventType
{
    Logon,
    Logoff,
    SessionDisconnect,
    SessionReconnect,
    PostShell
}

Remarque: Assurez-vous que Visual Studio fonctionne avec les privilèges d'administrateur en cliquant avec le bouton droit de la souris sur votre raccourci Visual Studio et en cliquant sur run as administrator, sinon un System.UnauthorizedAccessException sera lancé lorsque le programme est exécuté.

16
Simon Mourier

Voici le code (tous résidant à l'intérieur d'une classe; dans mon cas, la classe héritante ServiceBase). Ceci est particulièrement utile si vous souhaitez également obtenir le nom d'utilisateur de l'utilisateur connecté.

    [DllImport("Wtsapi32.dll")]
    private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WtsInfoClass wtsInfoClass, out IntPtr ppBuffer, out int pBytesReturned);
    [DllImport("Wtsapi32.dll")]
    private static extern void WTSFreeMemory(IntPtr pointer);

    private enum WtsInfoClass
    {
        WTSUserName = 5, 
        WTSDomainName = 7,
    }

    private static string GetUsername(int sessionId, bool prependDomain = true)
    {
        IntPtr buffer;
        int strLen;
        string username = "SYSTEM";
        if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSUserName, out buffer, out strLen) && strLen > 1)
        {
            username = Marshal.PtrToStringAnsi(buffer);
            WTSFreeMemory(buffer);
            if (prependDomain)
            {
                if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
                {
                    username = Marshal.PtrToStringAnsi(buffer) + "\\" + username;
                    WTSFreeMemory(buffer);
                }
            }
        }
        return username;
    }

Avec le code ci-dessus dans votre classe, vous pouvez simplement obtenir le nom d'utilisateur dans la méthode que vous remplissez comme ceci:

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    string username = GetUsername(changeDescription.SessionId);
    //continue with any other thing you wish to do
}

NB: N'oubliez pas d'ajouter CanHandleSessionChangeEvent = true; Dans le constructeur de la classe héritante de ServiceBase

1
Soma Mbadiwe