2

GetLastInputInfo can work well on application but not a service.

GetLastInputInfo will always return LASTINPUTINFO.dwTime=0 since it is running on Session 0.

How can I achieve detecting the system is idle or not in Windows Service?

1 Answers1

1

Your service can enumerate the active sessions via WTSEnumerateSessions(), querying each session for its WTSSessionInfo/Ex via WTSQuerySessionInformation(). That will give you each session's LastInputTime (amongst other things).

PWTS_SESSION_INFO sessions = NULL;
DWORD count = 0;

if (!WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &sessions, &count))
{
    DWORD errCode = GetLastError();
    ...
}
else
{
    for (DWORD i = 0; i < count; ++i)
    {
        PWTSINFO info = NULL;
        DWORD size = 0;

        if (!WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, sessions[i].SessionId, WTSSessionInfo, (LPWSTR*)&info, &size))
        {
            DWORD errCode = GetLastError();
            ...
        }
        else
        {
            // use info->LastInputTime as needed...
            WTSFreeMemory(info);
        }
    }
    WTSFreeMemory(sessions);
}

If you are interested only in the session that is connected to the local machine's physical screen/keyboard/mouse, you can use WTSGetActiveConsoleSessionId() instead of WTSEnumerateSessions().

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thanks. I try to test it but all the value I got is ZERO. Why? ``` while (true) { auto id = WTSGetActiveConsoleSessionId(); auto hServer = WTS_CURRENT_SERVER_HANDLE; DWORD bytes = 0; WCHAR* buf = NULL; auto b = WTSQuerySessionInformationW(hServer, id, WTSSessionInfo, &buf, &bytes); auto info = (PWTSINFOW)(buf); cout << info->LastInputTime.LowPart << endl; cout << info->LastInputTime.HighPart << endl; cout << info->State << endl; WTSFreeMemory(buf); Sleep(1000); } ``` – James Cheng Sep 15 '20 at 15:54
  • You are not checking the return value of `WTSQuerySessionInformationW()` for failure before accessing the returned data. Try this: `while (true) { auto id = WTSGetActiveConsoleSessionId(); if (id != 0xFFFFFFFF) { DWORD bytes = 0; PWTSINFOW info = NULL; if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, id, WTSSessionInfo, (LPWSTR*)&info, &bytes)) { cout << "LastInputTime: " << info->LastInputTime.QuadPart << endl; cout << "State: " << info->State << endl; WTSFreeMemory(info); } else { auto errCode = GetLastError(); cout << "Error: " << errCode << endl; } } Sleep(1000); }` – Remy Lebeau Sep 15 '20 at 16:41
  • I've checked all the API returned success without error, so for making things simple, I don't put error checking here. But the code is still printing 0 on application and windows service. I cannot find any further resources to make it work. Is there any resource for this. Thanks – James Cheng Sep 16 '20 at 01:15
  • 2
    Are you having the same problem with `WTSSessionInfoEx`? If so, then I don't know what else to tell you about it. Clearly it is an issue you will have to take up with Microsoft. In the meantime, you could try using `WTSQueryUserToken()` and `CreateProcessAsUser()` to create a new process in each desired session, have that process call `GetLastInputInfo()`, and use IPC to send that info back to your main service process. – Remy Lebeau Sep 16 '20 at 01:24
  • Try setting "Allow service to interact with desktop" flag. – rustyx Sep 16 '20 at 21:39
  • @rustyx That flag has no effect in Vista onwards, due to Session 0 Isolation. – Remy Lebeau Sep 16 '20 at 21:40
  • @RemyLebeau CreateProcessAsUser is what I did yesterday (I don't like it since it must return the result by some IPC method...any recommended way to IPC back (I use exit code)?) I've tried WTSSessionInfoEx already, still get zero. – James Cheng Sep 17 '20 at 01:25
  • @JamesCheng There are many IPC mechanisms that work with services - named pipes, sockets, etc. The `LastInputTime` timestamp is too big to fit in a process exit code, but you could have the spawned child process write the `LastInputTime` to its `stdout`, and then the service can redirect and read from that `stdout` when calling `CreateProcessAsUser()`. See [Creating a Child Process with Redirected Input and Output](https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output). – Remy Lebeau Sep 17 '20 at 01:49
  • Confirmed: the only way is to `CreateProcessAsUser()` and IPC back. https://web.archive.org/web/20211106110931/https://3735943886.com/?p=80 – Andrey Moiseev Nov 06 '21 at 11:13
  • @AndreyMoiseev kind of curious that blog was posted *after* my last comment here, mentioning all of the different ways I had mentioned here. Though it does add another way, too - query the input time from WMI. – Remy Lebeau Nov 06 '21 at 17:13
  • @RemyLebeau Curious indeed. Yeah, they probably took the information from this post. They also used the `WTSGetActiveConsoleSessionId()` "shorter path" though, which does not work: https://stackoverflow.com/questions/8309043/wtsgetactiveconsolesessionid-returning-system-session – Andrey Moiseev Nov 07 '21 at 13:40