0

As discussed in WTSQueryUserToken throws error 1008, even when running under LocalSystem, I'm having trouble getting my Windows service to launch an interactive process on a particular user's desktop as soon as they log in.

The proposed solution there was to handle the SERVICE_CONTROL_SESSIONCHANGE control code and use the passed dwSessionId. Here's all of the code (apologies that it's quite lengthy, but I was told to post it here anyway):

// These headers just contain system header #include's function prototypes
// and global variable declarations. If a variable below seems like it is
// undefined, rest assured that it *is* defined in one of these headers.
#include "events.h"
#include "main.h"

int __cdecl _tmain(int argc, LPTSTR argv[]) {
    sysStart = system_clock::now();
    LogInit();

    // If command-line parameter is "install", install the service.
    // Otherwise, the service is probably being started by the SCM
    if (lstrcmpi(argv[1], L"install") == 0) {
        return SvcInstall();
    }

    SERVICE_TABLE_ENTRY dispatchTable[] = {
        { &svcName[0], (LPSERVICE_MAIN_FUNCTION)SvcMain },
        { nullptr, nullptr }
    };

    // This call returns when the service has stopped. The
    // process should simply terminate when the call returns
    if (!StartServiceCtrlDispatcher(dispatchTable)) {
        ReportSvcEvent("StartServiceCtrlDispatcher");
    }

    return ERROR_SUCCESS;
}

char* WINAPI GetTimestamp(string& buf) {
    int ms = (high_resolution_clock::now().
        time_since_epoch().count() / 1000000) % 1000;

    auto tt = system_clock::to_time_t(
        system_clock::now());
    tm time;
    localtime_s(&time, &tt);

    strftime(&buf[0], 21, "[%d-%m-%Y %T", &time);
    snprintf(&buf[0], 26, "%s.%03d] ", &buf[0], ms);
    buf[25] = ' ';

    return &buf[0];
}

bool WINAPI LaunchDebugger(void) {
    // Get System directory, typically C:\Windows\System32
    wstring systemDir(MAX_PATH + 1, '\0');
    UINT nChars = GetSystemDirectory(&systemDir[0], systemDir.length());

    if (nChars == 0) {
        return false; // failed to get system directory
    }

    systemDir.resize(nChars);

    // Get process ID and create the command line
    // wostringstream ss;
    // ss << systemDir << L"\\vsjitdebugger.exe -p " << GetCurrentProcessId();
    wstring cmdLine = L"";

    // Start debugger process
    STARTUPINFOW si;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);

    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));

    if (!CreateProcess(nullptr, &cmdLine[0], nullptr,
        nullptr, false, 0, nullptr, nullptr, &si, &pi)) {
        return false;
    }

    // Close debugger process handles to eliminate resource leaks
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    // Wait for the debugger to attach
    while (!IsDebuggerPresent()) {
        Sleep(100);
    }

    // Stop execution so the debugger can take over
    DebugBreak();
    return true;
}

VOID WINAPI LogActiveTime(void) {
    // The computer is shutting down - write an entry to logFile to reflect
    // this, prefixed with a null byte to mark the current file position
    // (used for parsing in the timestamp on the next system boot)
    logFile << '\0';
    LogMessage("User action", "System shutting down after being "
        "active for " + DurationString(system_clock::now() - sysStart));
    logFile.close();

    // If the log file contains > 40 lines (10 boot/shutdown cycles),
    // remove the first 4 lines (the earliest boot/shutdown cycle).
    // This stops the file from getting too long to read easily
    ifstream inFile(logFilePath);
    string line;
    auto count = 0;

    while (getline(inFile, line)) {
        count++;
    }
}

DWORD WINAPI LogError(const string& funcName) {
    auto err = 0;
    LogMessage(funcName, system_category(
        ).message(err = GetLastError()), true);
    return err;
}

DWORD WINAPI LogInactiveTime(void) {
    // Create a new log file to be used as the input on the next run
    LogInit("temp");

    // Open the existing log file for reading and find the last shutdown
    // log entry by copying its contents to the new file until a null byte
    // or EOF is found (see LogActiveTime() for more info)
    ifstream inFile(logFilePath);
    if (!inFile) {
        return LogError("LogInactiveTime");
    }

    char ch = inFile.get();
    while (ch != '\0' && !inFile.eof()) {
        logFile << ch;
        ch = inFile.get();
    }

    if (inFile.eof()) {
        // No shutdown log entry was found, i.e. this is probably the first
        // time the service has run on the current instance of our log file.
        // Close the temp file and re-open the original log file before
        // returning, otherwise future messages won't make it to the file!
        LogInit();
        return ERROR_SUCCESS;
    }

    // At this point we can be sure that a valid shutdown log entry
    // exists, so we now need to parse it into a chrono::time_point.
    // Also save the entry's starting position in pos for later use
    auto pos = inFile.tellg();
    auto tt = system_clock::to_time_t(sysStart);
    tm start, end = { 0 };

    localtime_s(&start, &tt);
    inFile >> get_time(&end, "[%d-%m-%Y %T");

    if (inFile.fail() || inFile.bad()) {
        return LogError("LogInactiveTime");
    }

    // Ensure that both time_points refer to
    // the correct time, regardless of DST
    end.tm_isdst = start.tm_isdst;
    sysEnd = system_clock::from_time_t(mktime(&end));

    // Go back to the *actual* start of the shutdown
    // log entry so we can copy it into the new file
    inFile.seekg(pos);

    // Finish copying over the rest of our existing log
    // file, then close it and replace it with the new one
    ch = inFile.get();
    while (!inFile.eof()) {
        logFile << ch;
        ch = inFile.get();
    }

    inFile.close();
    remove(logFilePath.c_str());

    logFile.close();
    rename("temp", logFilePath.c_str());

    // Finally, do what we *actually* came here to do!
    LogMessage("User action", "System booting after being "
        "inactive for " + DurationString(sysStart - sysEnd));
    return ERROR_SUCCESS;
}

VOID WINAPI LogInit(const string& filePath) {
    setlocale(LC_ALL, "en_US.UTF8");

    if (logFile.is_open()) {
        logFile.close();
    }

    logFile.open(filePath == "" ?
        logFilePath : filePath, ios::app);

    if (!logFile) {
        exit(GetLastError());
    }
}

VOID WINAPI LogMessage(const string& funcName,
    const string& msg, bool isError) {
    if (!logFile.is_open()) {
        LogInit();
    }

    string buf(52, '\0');
    snprintf(&buf[0], 52, "%s%-6s %-18s ", GetTimestamp(buf),
        isError ? "ERROR:" : "INFO:", &(funcName + ':')[0]);
    buf[51] = ' ';

    logFile << buf << msg << endl;
}

VOID WINAPI ReportSvcEvent(const string& funcName) {
    HANDLE eventSrc = RegisterEventSource(nullptr, &svcName[0]);
    if (eventSrc != nullptr) {
        LPCSTR errParams[2] = { "WinUtilities" };
        char buf[MAX_PATH];

        StringCchPrintfA(buf, MAX_PATH, "Function '%s' failed: %s",
            funcName.c_str(), system_category().message(GetLastError(
            )).c_str());
        errParams[1] = buf;

        ReportEventA(eventSrc,      // event log handle
            EVENTLOG_ERROR_TYPE,    // event type
            0,                      // event category
            SVC_ERROR,              // event identifier
            nullptr,                // no security identifier
            2,                      // size of lpszStrings array
            0,                      // no binary data
            errParams,              // array of strings
            nullptr);               // no binary data

        DeregisterEventSource(eventSrc);
    }
}

VOID WINAPI ReportSvcStatus(DWORD newState,
    DWORD exitCode, DWORD waitHint) {
    static DWORD dwCheckPoint = 1;
    static unordered_map<int, string> svcStates;

    if (svcStates.empty()) {
        // Initialise mapping from service state codes to readable strings
        svcStates.insert({ SERVICE_STOPPED, "Stopped" });
        svcStates.insert({ SERVICE_START_PENDING, "Start Pending" });
        svcStates.insert({ SERVICE_STOP_PENDING, "Stop Pending" });
        svcStates.insert({ SERVICE_RUNNING, "Running" });
        svcStates.insert({ SERVICE_CONTINUE_PENDING, "Continue Pending" });
        svcStates.insert({ SERVICE_PAUSE_PENDING, "Pause Pending" });
        svcStates.insert({ SERVICE_PAUSED, "Paused" });
    }

    // Update the SERVICE_STATUS structure with the new passed-in values
    svcStatus.dwCurrentState = newState;
    svcStatus.dwWin32ExitCode = exitCode;
    svcStatus.dwWaitHint = waitHint;

    if (newState == SERVICE_START_PENDING) {
        svcStatus.dwControlsAccepted = 0;
    } else {
        svcStatus.dwControlsAccepted =
            SERVICE_ACCEPT_SESSIONCHANGE |
            SERVICE_ACCEPT_STOP |
            SERVICE_ACCEPT_PRESHUTDOWN;
    }

    if (newState == SERVICE_RUNNING ||
        newState == SERVICE_STOPPED) {
        svcStatus.dwCheckPoint = 0;
    } else {
        svcStatus.dwCheckPoint = dwCheckPoint++;
    }

    // Report the status of the service to the SCM and our log file
    if (!SetServiceStatus(statusHandle, &svcStatus)) {
        LogError("SetServiceStatus");
    } else {
        LogMessage("SetServiceStatus", "Service status " \
            "updated to '" + svcStates[newState] + "'.");
    }
}

DWORD WINAPI SvcCtrlHandler(DWORD ctrlCode, DWORD
    eventType, LPVOID eventData, LPVOID context) {
    switch (ctrlCode) {
        case SERVICE_CONTROL_SESSIONCHANGE: {
            auto sessionId = ((WTSSESSION_NOTIFICATION*
                )eventData)->dwSessionId;

            switch (eventType) {
                case WTS_SESSION_LOGON: {
                    string userName;
                    DWORD size;

                    WTSQuerySessionInformationA(WTS_CURRENT_SERVER_HANDLE, sessionId,
                        WTS_INFO_CLASS::WTSUserName, (LPSTR*)&userName[0], &size);
                    ReportSvcEvent("log on");

                    // A user has successfully logged on to the PC. Now we can start
                    // an interactive worker process under that user's account which
                    // will perform the actual work that we want to do
                    STARTUPINFO si = { 0 };
                    si.cb = sizeof(si);
                    si.wShowWindow = true;

                    HANDLE hToken;
                    if (!WTSQueryUserToken(sessionId, &hToken)) {
                        LogError("WTSQueryUserToken");
                        return ERROR_CALL_NOT_IMPLEMENTED;
                    }

                    wstring cmdLine = L"C:\\Path\\to\\my\\app.exe";
                    if (!CreateProcessAsUser(hToken, &cmdLine[0], nullptr, nullptr, nullptr,
                        false, CREATE_NO_WINDOW, nullptr, nullptr, &si, &workerProc)) {
                        LogError("CreateProcessAsUser");
                        return ERROR_CALL_NOT_IMPLEMENTED;
                    }

                    CloseHandle(hToken);
                    break;
                } default: {
                    break;
                }
            }

            break;
        } case SERVICE_CONTROL_STOP: {
            // Signal the service to stop
            ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
            SetEvent(svcStopEvent);
            break;
        } case SERVICE_CONTROL_PRESHUTDOWN: {
            LogActiveTime();
            break;
        } default: {
            return ERROR_CALL_NOT_IMPLEMENTED;
        }
    }

    return NO_ERROR;
}

VOID WINAPI SvcInit(DWORD argc, LPTSTR argv[]) {
    // Get the time at which the last shutdown occurred, and
    // log the duration for which the system was inactive
    if (LogInactiveTime() > 0) {
        return;
    }

    // Create an event. The control handler function (SvcCtrlHandler)
    // signals this event when it receives the stop control code
    svcStopEvent = CreateEvent(
        nullptr,    // default security attributes
        TRUE,       // manual reset event
        FALSE,      // not signaled
        nullptr);   // no name

    if (svcStopEvent == nullptr) {
        LogError("CreateEvent");
        ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
        return;
    }

    // Report running status when initialisation is complete
    ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);

    // Wait until our stop event has been signalled
    WaitForSingleObject(svcStopEvent, INFINITE);

    // Code execution won't reach here until the service has been
    // fully stopped. Report this to the SCM when it happens, then
    // terminate the worker process and clean up its handles
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);

    if (workerProc.hProcess) {
        TerminateProcess(workerProc.hProcess, 0);

        CloseHandle(workerProc.hProcess);
        CloseHandle(workerProc.hThread);
    }
}

DWORD WINAPI SvcInstall(void) {
    TCHAR path[MAX_PATH];
    if (!GetModuleFileName(nullptr, path, MAX_PATH)) {
        return LogError("GetModuleFileName");
    }

    // Get a handle to the SCM database
    auto scm = OpenSCManager(
        nullptr,                 // local computer
        nullptr,                 // ServicesActive database
        SC_MANAGER_ALL_ACCESS);  // full access rights

    if (scm == nullptr) {
        return LogError("OpenSCManager");
    }

    // Create the service
    auto svc = CreateService(
        scm,                        // SCM database
        &svcName[0],                // name of service
        L"Windows Utilities",       // service name to display
        SERVICE_ALL_ACCESS,         // desired access
        SERVICE_WIN32_OWN_PROCESS,  // service type
        SERVICE_AUTO_START,         // start type
        SERVICE_ERROR_NORMAL,       // error control type
        path,                       // path to service's binary
        nullptr,                    // no load ordering group
        nullptr,                    // no tag identifier
        nullptr,                    // no dependencies
        nullptr,                    // LocalSystem account
        nullptr);                   // no password

    if (svc == nullptr) {
        CloseServiceHandle(scm);
        return LogError("CreateService");
    }

    SERVICE_DESCRIPTION sd;
    sd.lpDescription = const_cast<LPTSTR>(L"Logs system "
        "shutdown events to a text file on the desktop. "
        "Also creates a system-wide hot key to perform "
        "internet searches on any selected text.");

    if (!ChangeServiceConfig2(
        svc,                        // handle to service
        SERVICE_CONFIG_DESCRIPTION, // change: description
        &sd))                       // new description
    {
        CloseServiceHandle(svc);
        CloseServiceHandle(scm);

        return LogError("ChangeServiceConfig2");
    }

    CloseServiceHandle(svc);
    CloseServiceHandle(scm);

    LogMessage("SvcInstall", "Service installed successfully.");
    return ERROR_SUCCESS;
}

VOID WINAPI SvcMain(DWORD argc, LPTSTR argv[]) {
    // Register the handler function for the service
    statusHandle = RegisterServiceCtrlHandlerEx(
        &svcName[0], SvcCtrlHandler, 0);

    if (!statusHandle) {
        ReportSvcEvent("RegisterServiceCtrlHandlerEx");
        return;
    }

    // These SERVICE_STATUS members remain as set here
    svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    svcStatus.dwServiceSpecificExitCode = 0;

    // Report initial status to the SCM
    ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);

    // Perform service-specific initialization and work
    SvcInit(argc, argv);
}

The part that doesn't work is in the SvcCtrlHandler() function, where I'm trying to catch the aforementioned control code.

I've even gone so far as to rewrite this whole thing in C# (which is the language that I should have used in the first place since my code is soooooooooo much cleaner and clearer now) and guess what? I still have the exact same problem with the OnSessionChange() method!

When I cold boot the computer and allow my PC to autologin to my single user account, nothing happens (i.e. no app.exe started). But if I then log out and back in again, I get the results I'm looking for.

So it seems as though my service is one of the last few to load and this is stopping it from properly catching the SERVICE_CONTROL_SESSIONCHANGE control code. How can I fix this? MTIA! :D

Kenny83
  • 769
  • 12
  • 38
  • 1
    you can on service start, inside `SvcMain` (after `RegisterServiceCtrlHandlerEx` call) try get user token via `WTSQueryUserToken` and if ok here - just call `CreateProcessAsUserW`. if user already logged at this point - you got it token and start process, otherwise you receive notification in `SvcCtrlHandler` about user login – RbMm Nov 06 '20 at 15:46
  • Note, if you are going to go this way of manually detecting an existing login at startup, be sure to use `WTSEnumerateSessions()` and not `WTSGetActiveConsoleSessionId()`. – Remy Lebeau Nov 06 '20 at 17:59
  • "*So it seems as though my service is one of the last few to load and this is stopping it from properly catching the `SERVICE_CONTROL_SESSIONCHANGE` control code.*" - that is very unlikely. All services that are set to auto-start at OS startup will be started before allowing any users to login. What does your service registration look like? In any case, if you need an EXE auto-started on each user login, why not simply register the EXE in the Registry under the [`Run`](https://learn.microsoft.com/en-us/windows/win32/setupapi/run-and-runonce-registry-keys) key? – Remy Lebeau Nov 06 '20 at 17:59
  • @RemyLebeau In regards to your first comment above there's no mention of `WTSGetActiveConsoleSessionId()` anywhere in this question, so why are you even mentioning that?? There's a brief mention of that buried deep in the hidden comments on the linked question, but why do you think I made this one? Because I'm dropping my previous approach of course! – Kenny83 Nov 06 '20 at 19:05
  • @RemyLebeau Your second comment makes much more sense though, especially the part about auto-starting services running before users can login. This is what I automatically assumed before writing any of this code, but it seems we're both wrong. The rest of what you said seems like a valid workaround, but not a solution. And even if it's just to appease my curiosity, I'd like to know what's really going on here and how to fix it. If you don't know, please don't waste your valuable time making suggestions like this. And if you want to see my code, I've posted a link to it above. – Kenny83 Nov 06 '20 at 19:10
  • @Kenny83 "*so why are you even mentioning [WTSGetActiveConsoleSessionId]*" - because 1) you mentioned it in your [other question](https://stackoverflow.com/questions/64695743/) related to this same topic, and 2) I was just adding it as an FYI, since many users of the WTS API don't always realize that there can be multiple sessions logged into at the same time, and `WTSGetActiveConsoleSessionId()` is rarely the correct API to use in general. – Remy Lebeau Nov 06 '20 at 19:11
  • @Kenny83 "*This is what I automatically assumed before writing any of this code, but it seems we're both wrong*" - I have been writing Windows services for almost 20 years, and have never had an issue with an auto-start service starting AFTER a user had already logged in. "*if you want to see my code, I've posted a link to it above*" - please don't post links to code hosted on external sites. SO questions are expected to be self-contained. Links break over time. Please edit your question to show the relevant code directly. – Remy Lebeau Nov 06 '20 at 19:15
  • @RemyLebeau "*I was just adding it as an FYI*..." Fair enough, I sometimes forget that SO isn't just for helping me LOL. Apologies for that. – Kenny83 Nov 06 '20 at 19:23
  • @RemyLebeau "*Links break over time*". OK sorry you make a very good point there. I have edited the question accordingly. – Kenny83 Nov 06 '20 at 19:44

0 Answers0