¿Determinar mediante programación la duración de una estación de trabajo bloqueada?

111

¿Cómo se puede determinar, en código, cuánto tiempo está bloqueada la máquina?

Otras ideas fuera de C # también son bienvenidas.


Me gusta la idea del servicio de Windows (y la he aceptado) por su simplicidad y limpieza, pero desafortunadamente no creo que funcione para mí en este caso particular. Quería ejecutar esto en mi estación de trabajo en el trabajo en lugar de en casa (o además de en casa, supongo), pero está bloqueado bastante por cortesía del Departamento de Defensa. En realidad, esa es parte de la razón por la que estoy rodando el mío.

Lo escribiré de todos modos y veré si funciona. ¡Gracias a todos!

AgenteConundrum
fuente

Respuestas:

138

No había encontrado esto antes, pero desde cualquier aplicación puedes conectar un SessionSwitchEventHandler. Obviamente, su aplicación deberá estar ejecutándose, pero siempre que lo esté:

Microsoft.Win32.SystemEvents.SessionSwitch += new Microsoft.Win32.SessionSwitchEventHandler(SystemEvents_SessionSwitch);

void SystemEvents_SessionSwitch(object sender, Microsoft.Win32.SessionSwitchEventArgs e)
{
    if (e.Reason == SessionSwitchReason.SessionLock)
    { 
        //I left my desk
    }
    else if (e.Reason == SessionSwitchReason.SessionUnlock)
    { 
        //I returned to my desk
    }
}
Timothy Carter
fuente
3
Probado al 100% en Windows 7 x64 y Windows 10 x64.
Contango
¡Vaya, funciona increíble! sin errores sin excepciones, suave y limpio.
Mayer Spitzer
Esta es la forma correcta de hacerlo. Según este artículo de Microsoft , "No hay ninguna función a la que pueda llamar para determinar si la estación de trabajo está bloqueada". Debe supervisarse mediante SessionSwitchEventHandler.
JonathanDavidArndt
35

Crearía un servicio de Windows (un tipo de proyecto de Visual Studio 2005) que maneja el evento OnSessionChange como se muestra a continuación:

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    if (changeDescription.Reason == SessionChangeReason.SessionLock)
    { 
        //I left my desk
    }
    else if (changeDescription.Reason == SessionChangeReason.SessionUnlock)
    { 
        //I returned to my desk
    }
}

Qué y cómo registra la actividad en ese momento depende de usted, pero un servicio de Windows proporciona un acceso rápido y fácil a los eventos de Windows como inicio, apagado, inicio de sesión / salida, junto con los eventos de bloqueo y desbloqueo.

Timothy Carter
fuente
18

La siguiente solución utiliza la API Win32. OnSessionLock se llama cuando la estación de trabajo está bloqueada y OnSessionUnlock cuando está desbloqueada.

[DllImport("wtsapi32.dll")]
private static extern bool WTSRegisterSessionNotification(IntPtr hWnd,
int dwFlags);

[DllImport("wtsapi32.dll")]
private static extern bool WTSUnRegisterSessionNotification(IntPtr
hWnd);

private const int NotifyForThisSession = 0; // This session only

private const int SessionChangeMessage = 0x02B1;
private const int SessionLockParam = 0x7;
private const int SessionUnlockParam = 0x8;

protected override void WndProc(ref Message m)
{
    // check for session change notifications
    if (m.Msg == SessionChangeMessage)
    {
        if (m.WParam.ToInt32() == SessionLockParam)
            OnSessionLock(); // Do something when locked
        else if (m.WParam.ToInt32() == SessionUnlockParam)
            OnSessionUnlock(); // Do something when unlocked
    }

    base.WndProc(ref m);
    return;
}

void OnSessionLock() 
{
    Debug.WriteLine("Locked...");
}

void OnSessionUnlock() 
{
    Debug.WriteLine("Unlocked...");
}

private void Form1Load(object sender, EventArgs e)
{
    WTSRegisterSessionNotification(this.Handle, NotifyForThisSession);
}

// and then when we are done, we should unregister for the notification
//  WTSUnRegisterSessionNotification(this.Handle);
adeel825
fuente
1
Esta es una buena opción si encuentra que el evento SessionSwitch (de otras respuestas) no se activa (por ejemplo, su aplicación lo suprime).
kad81
Para los lectores futuros ... Creo que la anulación aquí viene de System.Windows.Forms.Form, ya que podría escribir una clase como esta: public class Form1: System.Windows.Forms.Form
granadaCoder
Esto funciona para mí cuando SystemEvents.SessionSwitch no lo hace
DCOPTimDowd
5

Sé que esta es una pregunta antigua, pero encontré un método para obtener el estado de bloqueo para una sesión determinada.

Encontré mi respuesta aquí, pero estaba en C ++, así que traduje todo lo que pude a C # para obtener el estado de bloqueo.

Así que aquí va:

static class SessionInfo {
    private const Int32 FALSE = 0;

    private static readonly IntPtr WTS_CURRENT_SERVER = IntPtr.Zero;

    private const Int32 WTS_SESSIONSTATE_LOCK = 0;
    private const Int32 WTS_SESSIONSTATE_UNLOCK = 1;

    private static bool _is_win7 = false;

    static SessionInfo() {
        var os_version = Environment.OSVersion;
        _is_win7 = (os_version.Platform == PlatformID.Win32NT && os_version.Version.Major == 6 && os_version.Version.Minor == 1);
    }

    [DllImport("wtsapi32.dll")]
    private static extern Int32 WTSQuerySessionInformation(
        IntPtr hServer,
        [MarshalAs(UnmanagedType.U4)] UInt32 SessionId,
        [MarshalAs(UnmanagedType.U4)] WTS_INFO_CLASS WTSInfoClass,
        out IntPtr ppBuffer,
        [MarshalAs(UnmanagedType.U4)] out UInt32 pBytesReturned
    );

    [DllImport("wtsapi32.dll")]
    private static extern void WTSFreeMemoryEx(
        WTS_TYPE_CLASS WTSTypeClass,
        IntPtr pMemory,
        UInt32 NumberOfEntries
    );

    private enum WTS_INFO_CLASS {
        WTSInitialProgram = 0,
        WTSApplicationName = 1,
        WTSWorkingDirectory = 2,
        WTSOEMId = 3,
        WTSSessionId = 4,
        WTSUserName = 5,
        WTSWinStationName = 6,
        WTSDomainName = 7,
        WTSConnectState = 8,
        WTSClientBuildNumber = 9,
        WTSClientName = 10,
        WTSClientDirectory = 11,
        WTSClientProductId = 12,
        WTSClientHardwareId = 13,
        WTSClientAddress = 14,
        WTSClientDisplay = 15,
        WTSClientProtocolType = 16,
        WTSIdleTime = 17,
        WTSLogonTime = 18,
        WTSIncomingBytes = 19,
        WTSOutgoingBytes = 20,
        WTSIncomingFrames = 21,
        WTSOutgoingFrames = 22,
        WTSClientInfo = 23,
        WTSSessionInfo = 24,
        WTSSessionInfoEx = 25,
        WTSConfigInfo = 26,
        WTSValidationInfo = 27,
        WTSSessionAddressV4 = 28,
        WTSIsRemoteSession = 29
    }

    private enum WTS_TYPE_CLASS {
        WTSTypeProcessInfoLevel0,
        WTSTypeProcessInfoLevel1,
        WTSTypeSessionInfoLevel1
    }

    public enum WTS_CONNECTSTATE_CLASS {
        WTSActive,
        WTSConnected,
        WTSConnectQuery,
        WTSShadow,
        WTSDisconnected,
        WTSIdle,
        WTSListen,
        WTSReset,
        WTSDown,
        WTSInit
    }

    public enum LockState {
        Unknown,
        Locked,
        Unlocked
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct WTSINFOEX {
        public UInt32 Level;
        public UInt32 Reserved; /* I have observed the Data field is pushed down by 4 bytes so i have added this field as padding. */
        public WTSINFOEX_LEVEL Data;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct WTSINFOEX_LEVEL {
        public WTSINFOEX_LEVEL1 WTSInfoExLevel1;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct WTSINFOEX_LEVEL1 {
        public UInt32 SessionId;
        public WTS_CONNECTSTATE_CLASS SessionState;
        public Int32 SessionFlags;

        /* I can't figure out what the rest of the struct should look like but as i don't need anything past the SessionFlags i'm not going to. */

    }

    public static LockState GetSessionLockState(UInt32 session_id) {
        IntPtr ppBuffer;
        UInt32 pBytesReturned;

        Int32 result = WTSQuerySessionInformation(
            WTS_CURRENT_SERVER,
            session_id,
            WTS_INFO_CLASS.WTSSessionInfoEx,
            out ppBuffer,
            out pBytesReturned
        );

        if (result == FALSE)
            return LockState.Unknown;

        var session_info_ex = Marshal.PtrToStructure<WTSINFOEX>(ppBuffer);

        if (session_info_ex.Level != 1)
            return LockState.Unknown;

        var lock_state = session_info_ex.Data.WTSInfoExLevel1.SessionFlags;
        WTSFreeMemoryEx(WTS_TYPE_CLASS.WTSTypeSessionInfoLevel1, ppBuffer, pBytesReturned);

        if (_is_win7) {
            /* Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/ee621019(v=vs.85).aspx
                * Windows Server 2008 R2 and Windows 7:  Due to a code defect, the usage of the WTS_SESSIONSTATE_LOCK
                * and WTS_SESSIONSTATE_UNLOCK flags is reversed. That is, WTS_SESSIONSTATE_LOCK indicates that the
                * session is unlocked, and WTS_SESSIONSTATE_UNLOCK indicates the session is locked.
                * */
            switch (lock_state) {
                case WTS_SESSIONSTATE_LOCK:
                    return LockState.Unlocked;

                case WTS_SESSIONSTATE_UNLOCK:
                    return LockState.Locked;

                default:
                    return LockState.Unknown;
            }
        }
        else {
            switch (lock_state) {
                case WTS_SESSIONSTATE_LOCK:
                    return LockState.Locked;

                case WTS_SESSIONSTATE_UNLOCK:
                    return LockState.Unlocked;

                default:
                    return LockState.Unknown;
            }
        }
    }
}

Nota: El código anterior se extrajo de un proyecto mucho más grande, así que si me perdí un poco, lo siento. No he tenido tiempo de probar el código anterior, pero planeo volver en una semana o dos para comprobarlo todo. Solo lo publiqué ahora porque no quería olvidarme de hacerlo.

Robert
fuente
Esto funciona (Windows 7 probado hasta ahora). Gracias, hemos estado buscando esto durante las últimas semanas y su respuesta ha llegado justo a tiempo.
SteveP
1
Hay algunos errores en el código: 1. if (session_info_ex.Level != 1)- si la condición es verdadera, la memoria no se liberará. 2. si session_info_ex.Level! = 1 no debe hacer esto: Marshal.PtrToStructure<WTSINFOEX>(ppBuffer);porque el tamaño del búfer devuelto puede diferir del tamaño de WTSINFOEX
SergeyT
(continuación) 3. No fue necesario agregar el campo, UInt32 Reserved;sino que debe definir la estructura WTSINFOEX_LEVEL1completamente. En este caso, el compilador hará el relleno (alineación) correcto de los campos dentro de la estructura. 4. La función WTSFreeMemoryExse utiliza incorrectamente aquí. WTSFreeMemorydebe utilizarse en su lugar. WTSFreeMemoryExestá destinado a liberar memoria después WTSEnumerateSessionsEx.
SergeyT
(countinued) 5. CharSet = CharSet.Autodebe usarse en todos los atributos.
SergeyT
4

Si está interesado en escribir un servicio de Windows para "encontrar" estos eventos, topshelf (la biblioteca / marco que facilita mucho la escritura de servicios de Windows) tiene un gancho.

public interface IMyServiceContract
{
    void Start();

    void Stop();

    void SessionChanged(Topshelf.SessionChangedArguments args);
}



public class MyService : IMyServiceContract
{

    public void Start()
    {
    }

    public void Stop()
    {

    }

    public void SessionChanged(SessionChangedArguments e)
    {
        Console.WriteLine(e.ReasonCode);
    }   
}

y ahora el código para conectar el servicio de estante superior a la interfaz / concreto arriba

Todo lo que se muestra a continuación es una configuración "típica" de estante superior ... excepto por 2 líneas que marqué como

/ * ESTA ES MAGIC LINE * /

Esos son los que hacen que se active el método SessionChanged.

Probé esto con Windows 10 x64. Bloqueé y desbloqueé mi máquina y obtuve el resultado deseado.

            IMyServiceContract myServiceObject = new MyService(); /* container.Resolve<IMyServiceContract>(); */


            HostFactory.Run(x =>
            {
                x.Service<IMyServiceContract>(s =>
                {
                    s.ConstructUsing(name => myServiceObject);
                    s.WhenStarted(sw => sw.Start());
                    s.WhenStopped(sw => sw.Stop());
                    s.WhenSessionChanged((csm, hc, chg) => csm.SessionChanged(chg)); /* THIS IS MAGIC LINE */
                });

                x.EnableSessionChanged(); /* THIS IS MAGIC LINE */

                /* use command line variables for the below commented out properties */
                /*
                x.RunAsLocalService();
                x.SetDescription("My Description");
                x.SetDisplayName("My Display Name");
                x.SetServiceName("My Service Name");
                x.SetInstanceName("My Instance");
                */

                x.StartManually(); // Start the service manually.  This allows the identity to be tweaked before the service actually starts

                /* the below map to the "Recover" tab on the properties of the Windows Service in Control Panel */
                x.EnableServiceRecovery(r =>
                {
                    r.OnCrashOnly();
                    r.RestartService(1); ////first
                    r.RestartService(1); ////second
                    r.RestartService(1); ////subsequents
                    r.SetResetPeriod(0);
                });

                x.DependsOnEventLog(); // Windows Event Log
                x.UseLog4Net();

                x.EnableShutdown();

                x.OnException(ex =>
                {
                    /* Log the exception */
                    /* not seen, I have a log4net logger here */
                });
            });                 

My packages.config para proporcionar sugerencias sobre las versiones:

  <package id="log4net" version="2.0.5" targetFramework="net45" />
  <package id="Topshelf" version="4.0.3" targetFramework="net461" />
  <package id="Topshelf.Log4Net" version="4.0.3" targetFramework="net461" />
granadaCoder
fuente
o es posible usarlo x.EnableSessionChanged();junto con la ServiceSessionChangeimplementación de la interfaz si ha implementado ServiceControly no crea una instancia de clase de servicio implícita. Al igual x.Service<ServiceImpl>();. Tienes que implementar ServiceSessionChangeen ServiceImplclase:class ServiceImpl : ServiceControl, ServiceSessionChange
oleksa
3

NOTA : Esta no es una respuesta, sino una (contribución) a la respuesta de Timothy Carter , porque mi reputación no me permite comentar hasta ahora.

En caso de que alguien haya probado el código de la respuesta de Timothy Carter y no haya logrado que funcione de inmediato en un servicio de Windows, hay una propiedad que debe configurarse trueen el constructor del servicio. Simplemente agregue la línea en el constructor:

CanHandleSessionChangeEvent = true;

Y asegúrese de no establecer esta propiedad después de que se inicie el servicio, de lo contrario InvalidOperationException, se lanzará un error.

Abdul Rahman Kayali
fuente
-3

A continuación se muestra el código de trabajo 100% para saber si la PC está bloqueada o no.

Antes de usar esto, use el espacio de nombres System.Runtime.InteropServices.

[DllImport("user32", EntryPoint = "OpenDesktopA", CharSet = CharSet.Ansi,SetLastError = true, ExactSpelling = true)]
private static extern Int32 OpenDesktop(string lpszDesktop, Int32 dwFlags, bool fInherit, Int32 dwDesiredAccess);

[DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern Int32 CloseDesktop(Int32 hDesktop);

[DllImport("user32", CharSet = CharSet.Ansi,SetLastError = true,ExactSpelling = true)]
private static extern Int32 SwitchDesktop(Int32 hDesktop);

public static bool IsWorkstationLocked()
{
    const int DESKTOP_SWITCHDESKTOP = 256;
    int hwnd = -1;
    int rtn = -1;

    hwnd = OpenDesktop("Default", 0, false, DESKTOP_SWITCHDESKTOP);

    if (hwnd != 0)
    {
        rtn = SwitchDesktop(hwnd);
        if (rtn == 0)
        {
            // Locked
            CloseDesktop(hwnd);
            return true;
        }
        else
        {
            // Not locked
            CloseDesktop(hwnd);
        }
    }
    else
    {
        // Error: "Could not access the desktop..."
    }

    return false;
}
Luna de la década
fuente
4
Compruebe MSDN para OpenInputDesktop y GetUserObjectInformation, para obtener el nombre del escritorio activo en su lugar. El código anterior no es seguro / agradable para los usuarios que trabajan en varios escritorios, utilizando la utilidad desktops.exe de Microsoft o de otra manera. O mejor aún, intente crear una ventana en el escritorio activo (SetThreadDesktop) y, si funciona, muestre su interfaz de usuario en ella. Si no, entonces es un escritorio protegido / especial, así que no lo hagas.
eselk