Determinare a livello di programmazione una durata di una workstation bloccata?


111

Come si può determinare, in codice, per quanto tempo la macchina è bloccata?

Sono benvenute anche altre idee al di fuori di C #.


Mi piace l'idea del servizio Windows (e l'ho accettata) per semplicità e pulizia, ma sfortunatamente non credo che funzionerà per me in questo caso particolare. Volevo eseguirlo sulla mia workstation al lavoro piuttosto che a casa (o in aggiunta a casa, suppongo), ma è stato bloccato piuttosto duro per gentile concessione del DoD. Questo è uno dei motivi per cui sto girando il mio, in realtà.

Lo scriverò comunque e vedrò se funziona. Grazie a tutti!

Risposte:


138

Non l'avevo trovato prima, ma da qualsiasi applicazione puoi collegare un SessionSwitchEventHandler. Ovviamente la tua applicazione dovrà essere in esecuzione, ma fintanto che è:

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

3
Testato al 100% su Windows 7 x64 e Windows 10 x64.
Contango

Wow, funziona alla grande! nessun errore nessuna eccezione, liscio e pulito!
Mayer Spitzer

Questo è il modo giusto per farlo. Secondo questo articolo di Microsoft , "non è possibile chiamare alcuna funzione per determinare se la workstation è bloccata". Deve essere monitorato utilizzando SessionSwitchEventHandler.
JonathanDavidArndt

35

Creerei un servizio Windows (un tipo di progetto di Visual Studio 2005) che gestisce l'evento OnSessionChange come mostrato di seguito:

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

Spetta a te decidere cosa e come registrare l'attività a quel punto, ma un servizio di Windows fornisce un accesso rapido e semplice agli eventi di Windows come avvio, arresto, accesso / uscita, insieme agli eventi di blocco e sblocco.


18

La soluzione seguente utilizza l'API Win32. OnSessionLock viene chiamato quando la workstation è bloccata e OnSessionUnlock viene chiamato quando è sbloccato.

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

1
Questa è una buona opzione se trovi che l'evento SessionSwitch (da altre risposte) non si attiva (ad esempio la tua applicazione lo sopprime).
kad81

Per i lettori futuri ... Penso che l'override qui provenga da System.Windows.Forms.Form, poiché potresti scrivere una classe come questa: public class Form1: System.Windows.Forms.Form
granadaCoder

Questo funziona per me quando SystemEvents.SessionSwitch no
DCOPTimDowd

5

So che questa è una vecchia domanda, ma ho trovato un metodo per ottenere lo stato di blocco per una determinata sessione.

Ho trovato la mia risposta qui ma era in C ++, quindi ho tradotto il più possibile in C # per ottenere lo stato di blocco.

Quindi ecco qui:

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: il codice sopra è stato estratto da un progetto molto più grande quindi se mi sono perso un po 'mi dispiace. Non ho tempo per testare il codice sopra, ma ho intenzione di tornare tra una o due settimane per controllare tutto. L'ho pubblicato solo ora perché non volevo dimenticarmi di farlo.


Funziona (Windows 7 testato finora). Grazie, lo stavamo cercando nelle ultime settimane e la tua risposta è arrivata in un attimo!
SteveP

1
Ci sono pochi errori nel codice: 1. if (session_info_ex.Level != 1)- se la condizione è vera, la memoria non verrà liberata. 2. se session_info_ex.Level! = 1 non dovresti farlo: Marshal.PtrToStructure<WTSINFOEX>(ppBuffer);perché la dimensione del buffer restituito potrebbe differire dalla dimensione di WTSINFOEX
SergeyT

(continua) 3. Non era necessario aggiungere il campo, UInt32 Reserved;invece dovresti definire la struttura WTSINFOEX_LEVEL1completamente. In questo caso il compilatore eseguirà il riempimento corretto (allineamento) dei campi all'interno della struttura. 4. La funzione WTSFreeMemoryExè utilizzata in modo improprio qui. WTSFreeMemorydeve essere usato invece. WTSFreeMemoryExha lo scopo di liberare memoria dopo WTSEnumerateSessionsEx.
SergeyT

(countinued) 5. CharSet = CharSet.Autodeve essere utilizzato in tutti gli attributi.
SergeyT

4

Se sei interessato a scrivere un servizio Windows per "trovare" questi eventi, topshelf (la libreria / framework che rende la scrittura di servizi Windows molto più semplice) ha un gancio.

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

e ora il codice per collegare il servizio topshelf all'interfaccia / concrete sopra

Tutto ciò che segue è una configurazione "tipica" del topshelf ... tranne 2 righe che ho contrassegnato come

/ * QUESTA È LA LINEA MAGICA * /

Questi sono ciò che fa scattare il metodo SessionChanged.

L'ho testato con Windows 10 x64. Ho bloccato e sbloccato la mia macchina e ho ottenuto il risultato desiderato.

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

Il mio packages.config per fornire suggerimenti sulle versioni:

  <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" />

oppure è possibile utilizzarlo x.EnableSessionChanged();insieme ServiceSessionChangeall'implementazione dell'interfaccia se è stata implementata ServiceControle non si crea l'implicità dell'istanza della classe di servizio. Mi piace x.Service<ServiceImpl>();. Devi implementare ServiceSessionChangein ServiceImplclasse:class ServiceImpl : ServiceControl, ServiceSessionChange
oleksa

3

NOTA : Questa non è una risposta, ma un (contributo) alla risposta di Timothy Carter , perché la mia reputazione non mi permette di commentare finora.

Nel caso in cui qualcuno abbia provato il codice dalla risposta di Timothy Carter e non è riuscito a farlo funzionare immediatamente in un servizio Windows, c'è una proprietà che deve essere impostata truenel costruttore del servizio. Basta aggiungere la riga nel costruttore:

CanHandleSessionChangeEvent = true;

E assicurati di non impostare questa proprietà dopo l'avvio del servizio, altrimenti InvalidOperationExceptionverrà generato un messaggio.


-3

Di seguito è riportato il codice funzionante al 100% per scoprire se il PC è bloccato o meno.

Prima di usarlo usa lo spazio dei nomi 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;
}

4
Controlla MSDN per OpenInputDesktop e GetUserObjectInformation, per ottenere invece il nome del desktop attivo. Il codice sopra non è sicuro / piacevole per gli utenti che lavorano su più desktop, utilizzando l'utilità desktops.exe di Microsoft o altro. O meglio ancora, prova a creare una finestra sul desktop attivo (SetThreadDesktop) e, se funziona, mostra la tua interfaccia utente su di esso. In caso contrario, si tratta di un desktop protetto / speciale, quindi non farlo.
eselk
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.