web-dev-qa-db-ja.com

ログオンとログオフから通知を受け取る

ローカルPCでサービスとして実行されるプログラムを開発し、ユーザーステータスをサーバーに送信する必要があります。最初に、ユーザーlogonおよびlogoffを検出する必要があります。

私のアイデアは、ManagementEventWatcherクラスを使用し、Win32_LogonSessionにクエリを実行して、何かが変更された場合に通知を受けることでした。

私の最初のテストはうまくいきます、これがコード部分です(これはサービスからスレッドとして実行されます)

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

しかし、私は問題を理解しています。これがその課題を解決するための一般的な方法であるかどうかはわかりません。

  1. Win32_LogonSessionをクエリすると、同じユーザーに関連付けられている複数のレコードが表示されます。たとえば、このID 7580798と7580829を取得し、クエリを実行すると

    {Win32_LogonSession.LogonId = X}のアソシエーターWHERE ResultClass = Win32_UserAccount

    異なるIDで同じレコードを取得します。 (Win32_UserAccount.Domain = "PC-Name"、Name = "User1")

    同じユーザーで複数のログオンセッションがあるのはなぜですか?現在ログインしているユーザーを取得する一般的な方法は何ですか?または、ユーザーのログインによって正しく通知を受け取るにはどうすればよいですか?

  2. 同じように__InstanceDeletionEventを使用して、ユーザーがログオフしているかどうかを判断できると思いました。しかし、イベントが発生した場合、その後Win32_UserAccountにユーザー名を問い合わせることはできません。私は正しい?

私は正しい方向にいますか、それとももっと良い方法がありますか?あなたが私を助けることができればそれは素晴らしいでしょう!

編集 WTSRegisterSessionNotificationクラスは正しい方法ですか?サービスにはウィンドウハンドラがないため、それが可能かどうかはわかりません。

22

ServiceBase.OnSessionChange を使用して、さまざまなユーザーイベントをキャッチし、後で必要な情報を読み込みます。

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

セッション情報をロードするには、 WTS_INFO_CLASS を使用します。以下の私の例を参照してください:

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

次のコードは、AvailabilityChangedからの静的Userイベントを使用します。これは、セッション状態が変化するとすぐに発生します。 arg eには、特定のユーザーが含まれています。

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

サービスを利用しているため、セッション変更イベントを直接取得できます。

SERVICE_CONTROL_SESSIONCHANGEイベントを受信するように登録できます。特に、WTS_SESSION_LOGONWTS_SESSION_LOGOFFの理由を探す必要があります。

詳細および関連するMSDNドキュメントへのリンクについては、 昨日書いたこの回答 を確認してください。

C#ではさらに簡単です。ServiceBaseは既にサービスコントロールルーチンをラップし、イベントをオーバーライド可能なOnSessionChangeメソッドとして公開しているためです。 ServiceBaseのMSDNドキュメント を参照し、CanHandleSessionChangeEventプロパティをtrueに設定して、このメソッドの実行を有効にすることを忘れないでください。

フレームワークがOnSessionChangeオーバーライドを呼び出したときに返されるものは、理由(ログオフ、ログオンなど)を含む SessionChangeDescription Structure と、情報を取得するために使用できるセッションIDです。たとえば、ユーザーのログオン/ログオフ時(詳細については、前の回答へのリンクを参照してください)

編集:サンプルコード

 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

Windowsの一部である システムイベント通知サービス テクノロジを使用できます。ログオン/ログオフイベント(およびリモートセッション接続などの他のイベント)を提供する ISensLogon2インターフェイス があります。

これを行う方法を示すコード(サンプルコンソールアプリケーション)の一部を次に示します。たとえば、別のコンピューターからリモートデスクトップセッションを使用してテストできます。これにより、たとえば、SessionDisconnect、SessionReconnectイベントがトリガーされます。

このコードは、XPからWindows 8まで)のすべてのバージョンのWindowsをサポートする必要があります。

COM + 1.0 Admin Type Library別名COMAdminという名前のCOMコンポーネントへの参照を追加します。

組み込みの相互運用機能タイプを「False」に設定してください。そうしないと、次のエラーが発生します。「相互運用機能タイプ 'COMAdminCatalogClass'を埋め込めません。代わりに適切なインターフェースを使用してください。」

.NETでこのテクノロジーを使用することについてインターネットで目にする他の記事とは異なり、Sens.dllは参照されません。Windows8には存在しないようです(理由はわかりません)。ただし、テクノロジーはサポートされているようで、SENSサービスが実際にインストールされ、Windows 8で正常に実行されるため、インターフェースとGUIDを手動で宣言する(このサンプルのように)か、以前のバージョンのWindowsで作成された相互運用機能アセンブリを参照する必要があります(guidおよびさまざまなインターフェースは変更されていないため、正常に動作するはずです)。

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

注:Visual Studioのショートカットを右クリックして_run as administrator_をクリックし、それ以外の場合は_System.UnauthorizedAccessException_は、プログラムの実行時にスローされます。

16
Simon Mourier

コードは次のとおりです(すべてがクラス内にあります。私の場合、クラスはServiceBaseを継承しています)。これは、ログオンしているユーザーのユーザー名も取得する場合に特に便利です。

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

上記のコードをクラスに追加すると、オーバーライドするメソッドで次のようにユーザー名を取得できます。

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

注意:CanHandleSessionChangeEvent = true;ServiceBaseを継承するクラスのコンストラクター

1
Soma Mbadiwe