Recibe notificaciones de inicio y cierre de sesión

Tengo que desarrollar un progtwig que se ejecute en una PC local como servicio y entregue un par de estados de usuario a un servidor. Al principio tengo que detectar el inicio y cierre de sesión del usuario .

Mi idea fue usar la clase ManagementEventWatcher y consultar a Win32_LogonSession para recibir una notificación si algo cambió.

Mi primera prueba funciona bien, aquí está la parte del código (esto se ejecutaría como un hilo de un servicio) :

 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); } } 

Pero tengo algunos problemas de comprensión y no estoy seguro de si esta es la forma común de resolver esa tarea.

  1. Si Win32_LogonSession obtengo varios registros que están asociados al mismo usuario. Por ejemplo, obtengo estos ID 7580798 y 7580829 y si pregunto

    ASOCIADORES DE {Win32_LogonSession.LogonId = X} DONDE ResultClass = Win32_UserAccount

    Obtengo el mismo registro para diferentes identificaciones. (Win32_UserAccount.Domain = “PC-Name”, Name = “User1”)

    ¿Por qué hay varias sesiones de inicio de sesión con el mismo usuario? ¿Cuál es la forma común de obtener el usuario registrado actual? O mejor, ¿cómo recibir una notificación correcta por el inicio de sesión de un usuario?

  2. Pensé que podría usar lo mismo con __InstanceDeletionEvent para determinar si un usuario está desconectado. Pero supongo que si se produce el evento, no puedo consultar el nombre de usuario de Win32_UserAccount después de eso. ¿Estoy en lo cierto?

Estoy en la dirección correcta o hay mejores maneras? ¡Sería genial si pudieras ayudarme!

Editar ¿Es la clase WTSRegisterSessionNotification la forma correcta? No sé si es posible, porque en un servicio no tengo un manejador de ventanas.

Como está en un servicio, puede obtener eventos de cambio de sesión directamente.

Puedes registrarte para recibir el evento SERVICE_CONTROL_SESSIONCHANGE . En particular, deseará buscar las razones WTS_SESSION_LOGON y WTS_SESSION_LOGOFF .

Para obtener detalles y enlaces a los documentos relevantes de MSDN, verifique esta respuesta que escribí ayer .

En C # es aún más fácil, ya que ServiceBase ya envuelve la rutina de control de servicio y expone el evento como un método OnSessionChange anulable para usted. Consulte los documentos de MSDN para ServiceBase y no olvide configurar la propiedad CanHandleSessionChangeEvent en true para habilitar la ejecución de este método.

Lo que obtiene cuando el marco llama a su reemplazo de OnSessionChange es una estructura SessionChangeDescription con un motivo (cierre de sesión, inicio de sesión, …) y un ID de sesión que puede usar para obtener información, por ejemplo, sobre el inicio / cierre de sesión del usuario (consulte el enlace a mi respuesta anterior para más detalles)

EDITAR: código de muestra

  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; ... } 

Podría usar la tecnología del Servicio de notificación de eventos del sistema que forma parte de Windows. Tiene la interfaz ISensLogon2 que proporciona eventos de inicio / cierre de sesión (y otros eventos, como conexiones de sesión remota).

Aquí hay un fragmento de código (una aplicación de consola de muestra) que muestra cómo hacerlo. Puede probarlo usando una sesión de escritorio remoto desde otra computadora, por ejemplo, esto activará los eventos SessionDisconnect, SessionReconnect, por ejemplo.

Este código debe admitir todas las versiones de Windows desde XP a Windows 8.

Agregue una referencia al componente COM denominado, COM + 1.0 Admin Type Library, también conocido como COMAdmin.

Nota Asegúrese de configurar los Tipos de interoperabilidad incrustados en ‘Falso’, de lo contrario, obtendrá el siguiente error: “El tipo de interoperabilidad ‘COMAdminCatalogClass’ no se puede incrustar. Use la interfaz correspondiente en su lugar”.

Al contrario de otros artículos que encontrará en Internet sobre el uso de esta tecnología en .NET, no hace referencia a Sens.dll porque … parece que no existe en Windows 8 (no sé por qué). Sin embargo, parece que la tecnología es compatible y el servicio SENS está realmente instalado y funciona bien en Windows 8, por lo que solo necesita declarar las interfaces y las guías de forma manual (como en este ejemplo), o hacer referencia a un ensamblaje de interoperabilidad creado en una versión anterior de Windows (Debería funcionar bien ya que las guías y varias interfaces no han cambiado).

 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 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 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 } 

Nota: asegúrese de que Visual Studio se ejecute con privilegios de administrador haciendo clic con el botón derecho en su acceso directo de Visual Studio y haciendo clic en run as administrator , de lo contrario se lanzará una System.UnauthorizedAccessException cuando se ejecute el progtwig.

Aquí está el código (todos ellos residen dentro de una clase; en mi caso, la clase que hereda ServiceBase ). Esto es especialmente útil si también desea obtener el nombre de usuario del usuario que ha iniciado sesión.

  [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; } 

Con el código anterior en su clase, simplemente puede obtener el nombre de usuario en el método que está reemplazando de esta manera:

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

NB: recuerde agregar CanHandleSessionChangeEvent = true; En el constructor de la clase heredando de ServiceBase

Utilizo ServiceBase.OnSessionChange para capturar los diferentes eventos de usuario y luego cargar la información necesaria.

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

Para cargar la información de la sesión utilizo el WTS_INFO_CLASS . Vea mi ejemplo a continuación:

 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 User = new Dictionary(); 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 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); } } } 

El siguiente código utiliza el evento AvailabilityChanged estático del User , que se activa tan pronto como cambia el estado de la sesión. El argumento contiene el usuario específico.

 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); }