diff --git a/src/apps/deskflow-server/deskflow-server.cpp b/src/apps/deskflow-server/deskflow-server.cpp index ac660e590..5079f7199 100644 --- a/src/apps/deskflow-server/deskflow-server.cpp +++ b/src/apps/deskflow-server/deskflow-server.cpp @@ -28,6 +28,30 @@ int main(int argc, char **argv) Log log; EventQueue events; + // HACK: the `--active-desktop` arg actually belongs in the `deskflow-core` binary, + // but we are placing it here in the server binary temporarily until we are ready to + // ship the `deskflow-core` binary. we are deliberately not integrating `--active-desktop` + // into the existing `ServerApp` arg parsing code as that would be a waste of time. +#if SYSAPI_WIN32 + for (int i = 1; i < argc; ++i) { + std::string arg(argv[i]); + // This is called by the daemon (running in session 0) when it needs to know the name of the + // interactive desktop. + // It is necessary to run a utility process because the daemon runs in session 0, which does not + // have access to the active desktop, and so cannot query it's name. + if (arg == "--active-desktop") { + const auto name = ArchMiscWindows::getActiveDesktopName(); + if (name.empty()) { + LOG((CLOG_CRIT "failed to get active desktop name")); + return kExitFailed; + } + + LOG((CLOG_PRINT "%s", name.c_str())); + return kExitSuccess; + } + } +#endif + ServerApp app(&events); return app.run(argc, argv); } diff --git a/src/lib/arch/win32/ArchMiscWindows.cpp b/src/lib/arch/win32/ArchMiscWindows.cpp index 0107f781c..c2578b224 100644 --- a/src/lib/arch/win32/ArchMiscWindows.cpp +++ b/src/lib/arch/win32/ArchMiscWindows.cpp @@ -7,6 +7,7 @@ #include "arch/win32/ArchMiscWindows.h" #include "arch/win32/ArchDaemonWindows.h" +#include "arch/win32/XArchWindows.h" #include "base/Log.h" #include "common/constants.h" @@ -457,3 +458,19 @@ void ArchMiscWindows::setInstanceWin32(HINSTANCE instance) assert(instance != NULL); s_instanceWin32 = instance; } + +std::string ArchMiscWindows::getActiveDesktopName() +{ + HDESK desk = OpenInputDesktop(0, TRUE, GENERIC_READ); + if (desk == nullptr) { + LOG((CLOG_ERR "could not open input desktop")); + throw XArch(new XArchEvalWindows()); + } + + DWORD size; + GetUserObjectInformation(desk, UOI_NAME, nullptr, 0, &size); + auto *name = (TCHAR *)alloca(size + sizeof(TCHAR)); + GetUserObjectInformation(desk, UOI_NAME, name, size, &size); + CloseDesktop(desk); + return name; +} diff --git a/src/lib/arch/win32/ArchMiscWindows.h b/src/lib/arch/win32/ArchMiscWindows.h index adc13a3f8..b77f5cc45 100644 --- a/src/lib/arch/win32/ArchMiscWindows.h +++ b/src/lib/arch/win32/ArchMiscWindows.h @@ -155,12 +155,11 @@ public: static bool getParentProcessName(std::string &name); static HINSTANCE instanceWin32(); - static void setInstanceWin32(HINSTANCE instance); - static BOOL WINAPI getProcessEntry(PROCESSENTRY32 &entry, DWORD processID); static BOOL WINAPI getSelfProcessEntry(PROCESSENTRY32 &entry); static BOOL WINAPI getParentProcessEntry(PROCESSENTRY32 &entry); + static std::string getActiveDesktopName(); private: //! Open and return a registry key, closing the parent key diff --git a/src/lib/deskflow/DaemonApp.cpp b/src/lib/deskflow/DaemonApp.cpp index 884264e31..4679af1a2 100644 --- a/src/lib/deskflow/DaemonApp.cpp +++ b/src/lib/deskflow/DaemonApp.cpp @@ -376,7 +376,7 @@ void DaemonApp::handleIpcMessage(const Event &e, void *) } #if SYSAPI_WIN32 - std::string watchdogStatus = m_watchdog->isProcessActive() ? "active" : "idle"; + std::string watchdogStatus = m_watchdog->isProcessRunning() ? "active" : "idle"; LOG((CLOG_INFO "service status: %s", watchdogStatus.c_str())); #endif diff --git a/src/lib/platform/CMakeLists.txt b/src/lib/platform/CMakeLists.txt index 7c4223378..8f0ea2051 100644 --- a/src/lib/platform/CMakeLists.txt +++ b/src/lib/platform/CMakeLists.txt @@ -43,6 +43,8 @@ if(WIN32) MSWindowsKeyState.h MSWindowsPowerManager.cpp MSWindowsPowerManager.h + MSWindowsProcess.cpp + MSWindowsProcess.h MSWindowsScreen.cpp MSWindowsScreen.h MSWindowsScreenSaver.cpp diff --git a/src/lib/platform/MSWindowsProcess.cpp b/src/lib/platform/MSWindowsProcess.cpp new file mode 100644 index 000000000..c03d83f0c --- /dev/null +++ b/src/lib/platform/MSWindowsProcess.cpp @@ -0,0 +1,225 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#include "MSWindowsProcess.h" + +#include "arch/win32/XArchWindows.h" +#include "base/Log.h" +#include "ipc/IpcMessage.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#include + +#include + +namespace deskflow::platform { + +MSWindowsProcess::MSWindowsProcess(const std::string &command, HANDLE stdOutput, HANDLE stdError) + : m_command(command), + m_stdOutput(stdOutput), + m_stdError(stdError) +{ +} + +MSWindowsProcess::~MSWindowsProcess() +{ + if (m_createProcessResult) { + CloseHandle(m_info.hProcess); + CloseHandle(m_info.hThread); + } +} + +BOOL MSWindowsProcess::startInForeground() +{ + // clear, as we're reusing process info struct + ZeroMemory(&m_info, sizeof(PROCESS_INFORMATION)); + + // show the console window when in foreground mode, + // so we can close it gracefully, but minimize it + // so it doesn't get in the way. + STARTUPINFO si; + setStartupInfo(si); + si.dwFlags |= STARTF_USESHOWWINDOW; + si.wShowWindow = SW_MINIMIZE; + + m_createProcessResult = + CreateProcess(nullptr, LPSTR(m_command.c_str()), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &m_info); + return m_createProcessResult; +} + +BOOL MSWindowsProcess::startAsUser(HANDLE userToken, LPSECURITY_ATTRIBUTES sa) +{ + STARTUPINFO si; + setStartupInfo(si); + + LPVOID environment; + if (!CreateEnvironmentBlock(&environment, userToken, FALSE)) { + LOG((CLOG_ERR "could not create environment block")); + throw XArch(new XArchEvalWindows); + } + + ZeroMemory(&m_info, sizeof(PROCESS_INFORMATION)); + const DWORD creationFlags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT; + m_createProcessResult = CreateProcessAsUser( + userToken, nullptr, LPSTR(m_command.c_str()), sa, nullptr, TRUE, creationFlags, environment, nullptr, &si, &m_info + ); + + DestroyEnvironmentBlock(environment); + CloseHandle(userToken); + + return m_createProcessResult; +} + +void MSWindowsProcess::setStartupInfo(STARTUPINFO &si) +{ + static char g_desktopName[] = "winsta0\\Default"; // NOSONAR -- Idiomatic Win32 + + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = g_desktopName; + si.hStdOutput = m_stdOutput; + si.hStdError = m_stdError; + si.dwFlags |= STARTF_USESTDHANDLES; +} + +DWORD MSWindowsProcess::waitForExit() +{ + const auto kMaxWaitMilliseconds = 10000; + + LOG_INFO("waiting for process to exit, pid: %lu", m_info.dwProcessId); + if (WaitForSingleObject(m_info.hProcess, kMaxWaitMilliseconds) != WAIT_OBJECT_0) { + LOG_ERR("process did not exit within the expected time"); + TerminateProcess(m_info.hProcess, 1); + throw XArch(new XArchEvalWindows()); + } + + DWORD exitCode = 0; + if (!GetExitCodeProcess(m_info.hProcess, &exitCode)) { + LOG_ERR("failed to get exit code, error: %lu", GetLastError()); + throw XArch(new XArchEvalWindows()); + } + + if (exitCode != 0) { + LOG_WARN("process failed with exit code: %lu", exitCode); + } else { + LOG_DEBUG("process exited with code: %lu", exitCode); + } + + return exitCode; +} + +void MSWindowsProcess::shutdown(IpcServer &ipcServer, int timeout) +{ + shutdown(m_info.hProcess, m_info.dwProcessId, ipcServer, timeout); +} + +void MSWindowsProcess::shutdown(HANDLE handle, DWORD pid, IpcServer &ipcServer, int timeout) +{ + LOG_DEBUG("shutting down process %d", pid); + + DWORD exitCode; + GetExitCodeProcess(handle, &exitCode); + if (exitCode != STILL_ACTIVE) { + LOG_DEBUG("process %d is already shutdown", pid); + return; + } + + IpcShutdownMessage shutdown; + ipcServer.send(shutdown, IpcClientType::Node); + + // wait for process to exit gracefully. + double start = ARCH->time(); + while (true) { // NOSONAR -- Multiple breaks necessary + + GetExitCodeProcess(handle, &exitCode); + if (exitCode != STILL_ACTIVE) { + // yay, we got a graceful shutdown. there should be no hook in use errors! + LOG((CLOG_DEBUG "process %d was shutdown gracefully", pid)); + break; + } + + if (double elapsed = (ARCH->time() - start); elapsed > timeout) { + // if timeout reached, kill forcefully. + // calling TerminateProcess on deskflow is very bad! + // it causes the hook DLL to stay loaded in some apps, + // making it impossible to start deskflow again. + LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", (int)elapsed)); + TerminateProcess(handle, kExitSuccess); + break; + } + + ARCH->sleep(1); + } +} + +void MSWindowsProcess::createPipes() +{ + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = nullptr; + + if (!CreatePipe(&m_outputPipe, &m_stdOutput, &saAttr, 0)) { + LOG_ERR("could not create output pipe"); + throw XArch(new XArchEvalWindows()); + } + + if (!CreatePipe(&m_errorPipe, &m_stdError, &saAttr, 0)) { + LOG_ERR("could not create error pipe"); + throw XArch(new XArchEvalWindows()); + } + + // Set the pipes to non-blocking mode + DWORD mode = PIPE_NOWAIT; + if (!SetNamedPipeHandleState(m_outputPipe, &mode, nullptr, nullptr)) { + throw XArch(new XArchEvalWindows()); + } + + if (!SetNamedPipeHandleState(m_errorPipe, &mode, nullptr, nullptr)) { + throw XArch(new XArchEvalWindows()); + } +} + +std::string MSWindowsProcess::readStdOutput() +{ + return readOutput(m_outputPipe); +} + +std::string MSWindowsProcess::readStdError() +{ + return readOutput(m_errorPipe); +} + +std::string MSWindowsProcess::readOutput(HANDLE handle) +{ + const auto kOutputBufferSize = 4096; + char buffer[kOutputBufferSize]; // NOSONAR -- Idiomatic Win32 + + DWORD bytesRead; + DWORD totalBytesAvail; + DWORD bytesLeftThisMessage; + + // Check if there is data available in the pipe, which prevents `ReadFile` from freezing execution. + if (!PeekNamedPipe(handle, nullptr, 0, nullptr, &totalBytesAvail, &bytesLeftThisMessage)) { + LOG_ERR("could not peek into pipe"); + throw XArch(new XArchEvalWindows()); + } + + if (totalBytesAvail == 0) { + return ""; + } + + if (!ReadFile(handle, buffer, kOutputBufferSize, &bytesRead, nullptr)) { + LOG_ERR("could not read from pipe"); + throw XArch(new XArchEvalWindows()); + } + + return std::string(buffer, bytesRead); +} + +} // namespace deskflow::platform diff --git a/src/lib/platform/MSWindowsProcess.h b/src/lib/platform/MSWindowsProcess.h new file mode 100644 index 000000000..afa7c9a67 --- /dev/null +++ b/src/lib/platform/MSWindowsProcess.h @@ -0,0 +1,57 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * SPDX-FileCopyrightText: (C) 2025 Symless Ltd. + * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception + */ + +#pragma once + +#include "ipc/IpcServer.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#include + +namespace deskflow::platform { + +namespace { // NOSONAR -- Deliberate anonymous +const auto kDefaultShutdownTimeout = 10; +} + +class MSWindowsProcess +{ +public: + explicit MSWindowsProcess(const std::string &command, HANDLE stdOutput = nullptr, HANDLE stdError = nullptr); + ~MSWindowsProcess(); + + BOOL startInForeground(); + BOOL startAsUser(HANDLE userToken, LPSECURITY_ATTRIBUTES sa); + void shutdown(IpcServer &ipcServer, int timeout = kDefaultShutdownTimeout); + DWORD waitForExit(); + void createPipes(); + std::string readStdOutput(); + std::string readStdError(); + + PROCESS_INFORMATION info() const + { + return m_info; + } + + static void shutdown(HANDLE handle, DWORD pid, IpcServer &ipcServer, int timeout = kDefaultShutdownTimeout); + +private: + void setStartupInfo(STARTUPINFO &si); + + static std::string readOutput(HANDLE handle); + + std::string m_command; + HANDLE m_stdOutput; + HANDLE m_stdError; + HANDLE m_outputPipe = nullptr; + HANDLE m_errorPipe = nullptr; + PROCESS_INFORMATION m_info; + BOOL m_createProcessResult = FALSE; +}; + +} // namespace deskflow::platform diff --git a/src/lib/platform/MSWindowsSession.cpp b/src/lib/platform/MSWindowsSession.cpp index 6236f684d..4119708a8 100644 --- a/src/lib/platform/MSWindowsSession.cpp +++ b/src/lib/platform/MSWindowsSession.cpp @@ -158,23 +158,3 @@ BOOL MSWindowsSession::nextProcessEntry(HANDLE snapshot, LPPROCESSENTRY32 entry) return gotEntry; } - -std::string MSWindowsSession::getActiveDesktopName() -{ - std::string result; - try { - HDESK hd = OpenInputDesktop(0, TRUE, GENERIC_READ); - if (hd != NULL) { - DWORD size; - GetUserObjectInformation(hd, UOI_NAME, NULL, 0, &size); - TCHAR *name = (TCHAR *)alloca(size + sizeof(TCHAR)); - GetUserObjectInformation(hd, UOI_NAME, name, size, &size); - result = name; - CloseDesktop(hd); - } - } catch (std::exception &error) { - LOG((CLOG_ERR "failed to get active desktop name: %s", error.what())); - } - - return result; -} diff --git a/src/lib/platform/MSWindowsSession.h b/src/lib/platform/MSWindowsSession.h index 4f84be4f9..6b74e8cb2 100644 --- a/src/lib/platform/MSWindowsSession.h +++ b/src/lib/platform/MSWindowsSession.h @@ -19,7 +19,6 @@ public: MSWindowsSession(); ~MSWindowsSession(); - //! /*! Returns true if the session ID has changed since updateActiveSession was called. @@ -27,18 +26,14 @@ public: BOOL hasChanged(); bool isProcessInSession(const char *name, PHANDLE process); - HANDLE getUserToken(LPSECURITY_ATTRIBUTES security); + void updateActiveSession(); DWORD getActiveSessionId() { return m_activeSessionId; } - void updateActiveSession(); - - std::string getActiveDesktopName(); - private: BOOL nextProcessEntry(HANDLE snapshot, LPPROCESSENTRY32 entry); diff --git a/src/lib/platform/MSWindowsWatchdog.cpp b/src/lib/platform/MSWindowsWatchdog.cpp index 1bc29fcb9..be7e52210 100644 --- a/src/lib/platform/MSWindowsWatchdog.cpp +++ b/src/lib/platform/MSWindowsWatchdog.cpp @@ -10,7 +10,6 @@ #include "arch/Arch.h" #include "arch/win32/XArchWindows.h" #include "base/Log.h" -#include "base/String.h" #include "base/TMethodJob.h" #include "base/log_outputters.h" #include "common/ipc.h" @@ -31,9 +30,6 @@ #define CURRENT_PROCESS_ID 0 #define MAXIMUM_WAIT_TIME 3 -// TODO: maybe this should be \winlogon if we have logonui.exe? -static char g_desktopName[] = "winsta0\\Default"; - namespace { std::string trimDesktopName(const std::string &nameFromTraces) { @@ -50,17 +46,6 @@ std::string trimDesktopName(const std::string &nameFromTraces) return name; } -bool isDesktopRunnable(const std::string &desktopName) -{ - const std::string winlogon = "Winlogon"; - bool isNotLoginScreen = std::strncmp(desktopName.c_str(), winlogon.c_str(), winlogon.length()); - - const auto setting = ARCH->setting("runOnLoginScreen"); - bool runOnLoginScreen = (setting.empty() || setting == "true"); - - return (runOnLoginScreen || isNotLoginScreen); -} - } // namespace enum @@ -79,15 +64,14 @@ MSWindowsWatchdog::MSWindowsWatchdog( m_autoDetectCommand(autoDetectCommand), m_monitoring(true), m_commandChanged(false), - m_stdOutWrite(NULL), - m_stdOutRead(NULL), + m_outputWritePipe(nullptr), + m_outputReadPipe(nullptr), m_ipcServer(ipcServer), m_ipcLogOutputter(ipcLogOutputter), m_elevateProcess(false), m_processFailures(0), - m_processRunning(false), + m_processStarted(false), m_fileLogOutputter(NULL), - m_autoElevated(false), m_ready(false), m_foreground(foreground) { @@ -153,22 +137,30 @@ MSWindowsWatchdog::duplicateProcessToken(HANDLE process, LPSECURITY_ATTRIBUTES s } HANDLE -MSWindowsWatchdog::getUserToken(LPSECURITY_ATTRIBUTES security) +MSWindowsWatchdog::getUserToken(LPSECURITY_ATTRIBUTES security, bool elevatedToken) { + m_session.updateActiveSession(); + // always elevate if we are at the vista/7 login screen. we could also // elevate for the uac dialog (consent.exe) but this would be pointless, // since deskflow would re-launch as non-elevated after the desk switch, // and so would be unusable with the new elevated process taking focus. - if (m_elevateProcess || m_autoElevated || m_session.isProcessInSession("logonui.exe", NULL)) { + if (elevatedToken || m_session.isProcessInSession("logonui.exe", NULL)) { - LOG((CLOG_DEBUG "getting elevated token, %s", (m_elevateProcess ? "elevation required" : "at login screen"))); + LOG((CLOG_DEBUG "getting elevated token, %s", (elevatedToken ? "elevation required" : "at login screen"))); HANDLE process; if (!m_session.isProcessInSession("winlogon.exe", &process)) { throw XMSWindowsWatchdogError("cannot get user token without winlogon.exe"); } - return duplicateProcessToken(process, security); + try { + return duplicateProcessToken(process, security); + } catch (XArch &e) { + LOG_ERR("failed to duplicate user token from winlogon.exe"); + CloseHandle(process); + throw e; + } } else { LOG((CLOG_DEBUG "getting non-elevated token")); return m_session.getUserToken(security); @@ -177,12 +169,13 @@ MSWindowsWatchdog::getUserToken(LPSECURITY_ATTRIBUTES security) void MSWindowsWatchdog::mainLoop(void *) { + LOG_DEBUG("starting main loop"); shutdownExistingProcesses(); SendSas sendSasFunc = NULL; HINSTANCE sasLib = LoadLibrary("sas.dll"); if (sasLib) { - LOG((CLOG_DEBUG "found sas.dll")); + LOG((CLOG_DEBUG "loaded sas.dll, used to simulate ctrl-alt-del")); sendSasFunc = (SendSas)GetProcAddress(sasLib, "SendSAS"); } @@ -191,26 +184,24 @@ void MSWindowsWatchdog::mainLoop(void *) saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; - if (!CreatePipe(&m_stdOutRead, &m_stdOutWrite, &saAttr, 0)) { + if (!CreatePipe(&m_outputReadPipe, &m_outputWritePipe, &saAttr, 0)) { + LOG((CLOG_ERR "could not create output pipe")); throw XArch(new XArchEvalWindows()); } - ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION)); - while (m_monitoring) { try { - if (m_processRunning && getCommand().empty()) { - LOG((CLOG_INFO "process started but command is empty, shutting down")); + if (m_processStarted && getCommand().empty()) { + LOG((CLOG_DEBUG "process started but command is empty, shutting down")); shutdownExistingProcesses(); - m_processRunning = false; continue; } if (m_processFailures != 0) { // increasing backoff period, maximum of 10 seconds. int timeout = (m_processFailures * 2) < 10 ? (m_processFailures * 2) : 10; - LOG((CLOG_INFO "backing off, wait=%ds, failures=%d", timeout, m_processFailures)); + LOG((CLOG_WARN "backing off, wait=%ds, failures=%d", timeout, m_processFailures)); ARCH->sleep(timeout); } @@ -230,12 +221,12 @@ void MSWindowsWatchdog::mainLoop(void *) } } - if (m_processRunning && !isProcessActive()) { + if (m_processStarted && !isProcessRunning()) { m_processFailures++; - m_processRunning = false; + m_processStarted = false; - LOG((CLOG_WARN "detected application not running, pid=%d", m_processInfo.dwProcessId)); + LOG((CLOG_WARN "detected application not running, pid=%d", m_process->info().dwProcessId)); } if (sendSasFunc != NULL) { @@ -260,28 +251,34 @@ void MSWindowsWatchdog::mainLoop(void *) } catch (std::exception &e) { LOG((CLOG_CRIT "failed to launch, error: %s", e.what())); m_processFailures++; - m_processRunning = false; + m_processStarted = false; continue; } catch (...) { LOG((CLOG_CRIT "failed to launch, unknown error.")); m_processFailures++; - m_processRunning = false; + m_processStarted = false; continue; } } - if (m_processRunning) { + if (m_process != nullptr) { LOG((CLOG_DEBUG "terminated running process on exit")); - shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20); + m_process->shutdown(m_ipcServer); + m_process.reset(); + m_processStarted = false; } LOG((CLOG_DEBUG "watchdog main thread finished")); } -bool MSWindowsWatchdog::isProcessActive() +bool MSWindowsWatchdog::isProcessRunning() { + if (m_process == nullptr) { + return false; + } + DWORD exitCode; - GetExitCodeProcess(m_processInfo.hProcess, &exitCode); + GetExitCodeProcess(m_process->info().hProcess, &exitCode); return exitCode == STILL_ACTIVE; } @@ -292,64 +289,63 @@ void MSWindowsWatchdog::setFileLogOutputter(FileLogOutputter *outputter) void MSWindowsWatchdog::startProcess() { + LOG_INFO("daemon starting new process"); + if (m_command.empty()) { throw XMSWindowsWatchdogError("cannot start process, command is empty"); } m_commandChanged = false; - if (m_processRunning) { + if (m_process != nullptr) { LOG((CLOG_DEBUG "closing existing process to make way for new one")); - shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20); - m_processRunning = false; + m_process->shutdown(m_ipcServer); + m_process.reset(); + m_processStarted = false; } + m_process = std::make_unique(m_command, m_outputWritePipe, m_outputWritePipe); + BOOL createRet; if (m_foreground) { - LOG((CLOG_DEBUG "starting command in foreground")); - createRet = startProcessInForeground(m_command); + LOG_DEBUG("starting command in foreground"); + createRet = m_process->startInForeground(); } else { - LOG((CLOG_DEBUG "starting command as session user")); - m_session.updateActiveSession(); + LOG_DEBUG("starting new process in user session"); + + LOG_DEBUG("getting active desktop name"); + const auto activeDesktopName = runActiveDesktopUtility(); + + LOG_DEBUG("active desktop name: %s", activeDesktopName.c_str()); + // When we're at a UAC prompt, lock screen, or the login screen, Windows switches to the Winlogon desktop. + const auto isOnSecureDesktop = activeDesktopName == "Winlogon"; SECURITY_ATTRIBUTES sa; ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); + HANDLE userToken = getUserToken(&sa, isOnSecureDesktop || m_elevateProcess); - getActiveDesktop(&sa); - - if (!isDesktopRunnable(m_activeDesktop)) { - LOG((CLOG_INFO, "starting on the login screen is disabled")); - return; - } - - ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); - HANDLE userToken = getUserToken(&sa); - m_elevateProcess = m_autoElevated ? m_autoElevated : m_elevateProcess; - m_autoElevated = false; - - // patch by Jack Zhou and Henry Tung // set UIAccess to fix Windows 8 GUI interaction DWORD uiAccess = 1; SetTokenInformation(userToken, TokenUIAccess, &uiAccess, sizeof(DWORD)); - createRet = startProcessAsUser(m_command, userToken, &sa); + createRet = m_process->startAsUser(userToken, &sa); } if (!createRet) { LOG((CLOG_CRIT "could not launch command")); DWORD exitCode = 0; - GetExitCodeProcess(m_processInfo.hProcess, &exitCode); + GetExitCodeProcess(m_process->info().hProcess, &exitCode); LOG((CLOG_ERR "exit code: %d", exitCode)); throw XArch(new XArchEvalWindows); } else { // wait for program to fail. ARCH->sleep(1); - if (!isProcessActive()) { - closeProcessHandles(m_processInfo.dwProcessId); + if (!isProcessRunning()) { + m_process.reset(); throw XMSWindowsWatchdogError("process immediately stopped"); } - m_processRunning = true; + m_processStarted = true; m_processFailures = 0; LOG((CLOG_DEBUG "started core process from daemon")); @@ -360,70 +356,9 @@ void MSWindowsWatchdog::startProcess() } } -void MSWindowsWatchdog::setStartupInfo(STARTUPINFO &si) -{ - ZeroMemory(&si, sizeof(STARTUPINFO)); - si.cb = sizeof(STARTUPINFO); - si.lpDesktop = g_desktopName; - si.hStdError = m_stdOutWrite; - si.hStdOutput = m_stdOutWrite; - si.dwFlags |= STARTF_USESTDHANDLES; -} - -BOOL MSWindowsWatchdog::startProcessInForeground(std::string &command) -{ - // clear, as we're reusing process info struct - ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION)); - - // show the console window when in foreground mode, - // so we can close it gracefully, but minimize it - // so it doesn't get in the way. - STARTUPINFO si; - setStartupInfo(si); - si.dwFlags |= STARTF_USESHOWWINDOW; - si.wShowWindow = SW_MINIMIZE; - - BOOL result = CreateProcess(NULL, LPSTR(command.c_str()), NULL, NULL, TRUE, 0, NULL, NULL, &si, &m_processInfo); - - m_children.insert(std::make_pair(m_processInfo.dwProcessId, m_processInfo)); - - return result; -} - -BOOL MSWindowsWatchdog::startProcessAsUser(std::string &command, HANDLE userToken, LPSECURITY_ATTRIBUTES sa) -{ - // clear, as we're reusing process info struct - ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION)); - - STARTUPINFO si; - setStartupInfo(si); - - LPVOID environment; - BOOL blockRet = CreateEnvironmentBlock(&environment, userToken, FALSE); - if (!blockRet) { - LOG((CLOG_ERR "could not create environment block")); - throw XArch(new XArchEvalWindows); - } - - DWORD creationFlags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT; - - // re-launch in current active user session - LOG((CLOG_INFO "starting new process")); - BOOL createRet = CreateProcessAsUser( - userToken, NULL, LPSTR(command.c_str()), sa, NULL, TRUE, creationFlags, environment, NULL, &si, &m_processInfo - ); - - m_children.insert(std::make_pair(m_processInfo.dwProcessId, m_processInfo)); - - DestroyEnvironmentBlock(environment); - CloseHandle(userToken); - - return createRet; -} - void MSWindowsWatchdog::setCommand(const std::string &command, bool elevate) { - LOG((CLOG_INFO "service command updated")); + LOG_DEBUG("command parameters updated"); m_command = command; m_elevateProcess = elevate; m_commandChanged = true; @@ -463,7 +398,7 @@ void MSWindowsWatchdog::outputLoop(void *) while (m_monitoring) { DWORD bytesRead; - BOOL success = ReadFile(m_stdOutRead, buffer, kOutputBufferSize, &bytesRead, NULL); + BOOL success = ReadFile(m_outputReadPipe, buffer, kOutputBufferSize, &bytesRead, NULL); // assume the process has gone away? slow down // the reads until another one turns up. @@ -472,8 +407,6 @@ void MSWindowsWatchdog::outputLoop(void *) } else { buffer[bytesRead] = '\0'; - testOutput(buffer); - m_ipcLogOutputter.write(kINFO, buffer); if (m_fileLogOutputter != NULL) { @@ -493,48 +426,16 @@ void MSWindowsWatchdog::outputLoop(void *) } } -void MSWindowsWatchdog::shutdownProcess(HANDLE handle, DWORD pid, int timeout) -{ - DWORD exitCode; - GetExitCodeProcess(handle, &exitCode); - if (exitCode != STILL_ACTIVE) { - return; - } - - IpcShutdownMessage shutdown; - m_ipcServer.send(shutdown, IpcClientType::Node); - - // wait for process to exit gracefully. - double start = ARCH->time(); - while (true) { - - GetExitCodeProcess(handle, &exitCode); - if (exitCode != STILL_ACTIVE) { - // yay, we got a graceful shutdown. there should be no hook in use errors! - LOG((CLOG_DEBUG "process %d was shutdown gracefully", pid)); - break; - } else { - - double elapsed = (ARCH->time() - start); - if (elapsed > timeout) { - // if timeout reached, kill forcefully. - // calling TerminateProcess on deskflow is very bad! - // it causes the hook DLL to stay loaded in some apps, - // making it impossible to start deskflow again. - LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", (int)elapsed)); - TerminateProcess(handle, kExitSuccess); - break; - } - - ARCH->sleep(1); - } - } - - closeProcessHandles(pid); -} - void MSWindowsWatchdog::shutdownExistingProcesses() { + LOG_INFO("deamon shutting down existing processes"); + + if (m_process != nullptr) { + m_process->shutdown(m_ipcServer); + m_process.reset(); + m_processStarted = false; + } + // first we need to take a snapshot of the running processes HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, CURRENT_PROCESS_ID); if (snapshot == INVALID_HANDLE_VALUE) { @@ -565,7 +466,7 @@ void MSWindowsWatchdog::shutdownExistingProcesses() _stricmp(entry.szExeFile, "deskflow-core.exe") == 0) { HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID); - shutdownProcess(handle, entry.th32ProcessID, 10); + deskflow::platform::MSWindowsProcess::shutdown(handle, entry.th32ProcessID, m_ipcServer); CloseHandle(handle); } } @@ -578,64 +479,53 @@ void MSWindowsWatchdog::shutdownExistingProcesses() if (err != ERROR_NO_MORE_FILES) { // only worry about error if it's not the end of the snapshot - LOG((CLOG_ERR "could not get subsiquent process entry")); + LOG((CLOG_ERR "could not get next process entry")); throw XArch(new XArchEvalWindows); } } } - clearAllChildren(); CloseHandle(snapshot); - m_processRunning = false; } -void MSWindowsWatchdog::getActiveDesktop(LPSECURITY_ATTRIBUTES security) +std::string MSWindowsWatchdog::runActiveDesktopUtility() { - std::string installedDir = ARCH->getInstalledDirectory(); - if (!installedDir.empty()) { - MSWindowsSession session; - std::string name = session.getActiveDesktopName(); - if (name.empty()) { - LOG((CLOG_DEBUG "no active desktop in current session")); - } else { - LOG((CLOG_INFO "active desktop name: %s", name.c_str())); - } - } -} + const auto installDir = ARCH->getInstalledDirectory(); + const auto coreBinPath = installDir + "\\deskflow-server.exe"; + std::string utilityCommand = "\"" + coreBinPath + "\" --active-desktop"; -void MSWindowsWatchdog::testOutput(std::string buffer) -{ - // HACK: check standard output seems hacky. - size_t i = buffer.find(g_activeDesktop); - if (i != std::string::npos) { - size_t s = sizeof(g_activeDesktop); - std::string defaultScreen = "Default"; - m_activeDesktop = trimDesktopName(buffer.substr(i + s - 1)); - m_autoElevated = std::strncmp(m_activeDesktop.c_str(), defaultScreen.c_str(), defaultScreen.length()); + LOG_DEBUG("starting active desktop utility: %s", utilityCommand.c_str()); - ARCH->lockMutex(m_mutex); - m_ready = true; - ARCH->broadcastCondVar(m_condVar); - ARCH->unlockMutex(m_mutex); - } -} + SECURITY_ATTRIBUTES sa; + ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); + HANDLE userToken = getUserToken(&sa, true); -void MSWindowsWatchdog::closeProcessHandles(unsigned long pid, bool removeFromMap) -{ - auto processInfo = m_children.find(pid); - if (processInfo != m_children.end()) { - CloseHandle(processInfo->second.hProcess); - CloseHandle(processInfo->second.hThread); - if (removeFromMap) { - m_children.erase(processInfo); - } - } -} + deskflow::platform::MSWindowsProcess process(utilityCommand); + process.createPipes(); -void MSWindowsWatchdog::clearAllChildren() -{ - for (auto it = m_children.begin(); it != m_children.end(); ++it) { - closeProcessHandles(it->second.dwThreadId, false); + if (!process.startAsUser(userToken, &sa)) { + LOG_ERR("could not start active desktop process"); + throw XArch(new XArchEvalWindows()); } - m_children.clear(); + + LOG_DEBUG("started active desktop process, pid=%d", process.info().dwProcessId); + if (const auto exitCode = process.waitForExit(); exitCode != kExitSuccess) { + LOG_ERR("active desktop process, exit code: %d", exitCode); + throw XMSWindowsWatchdogError("could not get active desktop"); + } + + LOG_DEBUG("reading active desktop std error"); + if (const auto error = process.readStdError(); !error.empty()) { + LOG_WARN("active desktop process, error: %s", error.c_str()); + } + + LOG_DEBUG("reading active desktop std output"); + auto output = process.readStdOutput(); + if (output.empty()) { + LOG_ERR("could not get active desktop, no output"); + throw XMSWindowsWatchdogError("could not get active desktop"); + } + + output.erase(output.find_last_not_of("\r\n") + 1); + return output; } diff --git a/src/lib/platform/MSWindowsWatchdog.h b/src/lib/platform/MSWindowsWatchdog.h index 5e9c2aeaa..a08aff4e0 100644 --- a/src/lib/platform/MSWindowsWatchdog.h +++ b/src/lib/platform/MSWindowsWatchdog.h @@ -9,14 +9,15 @@ #include "arch/IArchMultithread.h" #include "deskflow/XDeskflow.h" +#include "platform/MSWindowsProcess.h" #include "platform/MSWindowsSession.h" -#include -#include - #define WIN32_LEAN_AND_MEAN #include +#include +#include + class Thread; class IpcLogOutputter; class IpcServer; @@ -32,36 +33,28 @@ public: std::string getCommand() const; void setCommand(const std::string &command, bool elevate); void stop(); - bool isProcessActive(); + bool isProcessRunning(); void setFileLogOutputter(FileLogOutputter *outputter); private: void mainLoop(void *); void outputLoop(void *); - void shutdownProcess(HANDLE handle, DWORD pid, int timeout); void shutdownExistingProcesses(); HANDLE duplicateProcessToken(HANDLE process, LPSECURITY_ATTRIBUTES security); - HANDLE getUserToken(LPSECURITY_ATTRIBUTES security); + HANDLE getUserToken(LPSECURITY_ATTRIBUTES security, bool elevatedToken); void startProcess(); - BOOL startProcessAsUser(std::string &command, HANDLE userToken, LPSECURITY_ATTRIBUTES sa); - BOOL startProcessInForeground(std::string &command); void sendSas(); - void getActiveDesktop(LPSECURITY_ATTRIBUTES security); - void testOutput(std::string buffer); void setStartupInfo(STARTUPINFO &si); - void checkChildren(); + /** - * @brief This closes the handles held to a child thread - * @param pid the ID of the process to kill, will do nothing if PID is not a - * valid child - * @param removeFromMap should the function remove the item from the children - * map + * @brief Re-run the process to get the active desktop name. + * + * It is necessary to run a utility process because the daemon runs in session 0, which does not + * have access to the active desktop, and so cannot query it's name. + * + * @return std::string The name of the active desktop. */ - void closeProcessHandles(unsigned long pid, bool removeFromMap = true); - /** - * @brief This kills off all children's handles created by this process - */ - void clearAllChildren(); + std::string runActiveDesktopUtility(); private: Thread *m_thread; @@ -69,28 +62,22 @@ private: std::string m_command; bool m_monitoring; bool m_commandChanged; - HANDLE m_stdOutWrite; - HANDLE m_stdOutRead; + HANDLE m_outputWritePipe; + HANDLE m_outputReadPipe; Thread *m_outputThread; IpcServer &m_ipcServer; IpcLogOutputter &m_ipcLogOutputter; bool m_elevateProcess; MSWindowsSession m_session; - PROCESS_INFORMATION m_processInfo; int m_processFailures; - bool m_processRunning; + bool m_processStarted; FileLogOutputter *m_fileLogOutputter; - bool m_autoElevated; ArchMutex m_mutex; ArchCond m_condVar; bool m_ready; bool m_foreground; std::string m_activeDesktop; - - /// @brief Save the info of all process made - /// We will use this to track all processes we make and - /// kill off handels and children that we no longer need - std::map m_children; + std::unique_ptr m_process; }; //! Relauncher error