diff options
| author | 2018-04-25 18:07:30 -0400 | |
|---|---|---|
| committer | 2018-04-25 18:07:30 -0400 | |
| commit | 9b1b081cfdb1c0fb6457278775e0823f8bc10f62 (patch) | |
| tree | ce8840148d8445055ba9e4f12263b2208f234c16 /src/lib/platform/MSWindowsWatchdog.cpp | |
Import Upstream version 2.0.0+dfsgupstream/2.0.0+dfsg
Diffstat (limited to 'src/lib/platform/MSWindowsWatchdog.cpp')
| -rw-r--r-- | src/lib/platform/MSWindowsWatchdog.cpp | 611 |
1 files changed, 611 insertions, 0 deletions
diff --git a/src/lib/platform/MSWindowsWatchdog.cpp b/src/lib/platform/MSWindowsWatchdog.cpp new file mode 100644 index 0000000..ba1890e --- /dev/null +++ b/src/lib/platform/MSWindowsWatchdog.cpp @@ -0,0 +1,611 @@ +/* + * barrier -- mouse and keyboard sharing utility + * Copyright (C) 2012-2016 Symless Ltd. + * Copyright (C) 2009 Chris Schoeneman + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "platform/MSWindowsWatchdog.h" + +#include "ipc/IpcLogOutputter.h" +#include "ipc/IpcServer.h" +#include "ipc/IpcMessage.h" +#include "ipc/Ipc.h" +#include "barrier/App.h" +#include "barrier/ArgsBase.h" +#include "mt/Thread.h" +#include "arch/win32/ArchDaemonWindows.h" +#include "arch/win32/XArchWindows.h" +#include "arch/Arch.h" +#include "base/log_outputters.h" +#include "base/TMethodJob.h" +#include "base/Log.h" +#include "common/Version.h" + +#include <sstream> +#include <UserEnv.h> +#include <Shellapi.h> + +#define MAXIMUM_WAIT_TIME 3 +enum { + kOutputBufferSize = 4096 +}; + +typedef VOID (WINAPI *SendSas)(BOOL asUser); + +const char g_activeDesktop[] = {"activeDesktop:"}; + +MSWindowsWatchdog::MSWindowsWatchdog( + bool daemonized, + bool autoDetectCommand, + IpcServer& ipcServer, + IpcLogOutputter& ipcLogOutputter) : + m_thread(NULL), + m_autoDetectCommand(autoDetectCommand), + m_monitoring(true), + m_commandChanged(false), + m_stdOutWrite(NULL), + m_stdOutRead(NULL), + m_ipcServer(ipcServer), + m_ipcLogOutputter(ipcLogOutputter), + m_elevateProcess(false), + m_processFailures(0), + m_processRunning(false), + m_fileLogOutputter(NULL), + m_autoElevated(false), + m_ready(false), + m_daemonized(daemonized) +{ + m_mutex = ARCH->newMutex(); + m_condVar = ARCH->newCondVar(); +} + +MSWindowsWatchdog::~MSWindowsWatchdog() +{ + if (m_condVar != NULL) { + ARCH->closeCondVar(m_condVar); + } + + if (m_mutex != NULL) { + ARCH->closeMutex(m_mutex); + } +} + +void +MSWindowsWatchdog::startAsync() +{ + m_thread = new Thread(new TMethodJob<MSWindowsWatchdog>( + this, &MSWindowsWatchdog::mainLoop, nullptr)); + + m_outputThread = new Thread(new TMethodJob<MSWindowsWatchdog>( + this, &MSWindowsWatchdog::outputLoop, nullptr)); +} + +void +MSWindowsWatchdog::stop() +{ + m_monitoring = false; + + m_thread->wait(5); + delete m_thread; + + m_outputThread->wait(5); + delete m_outputThread; +} + +HANDLE +MSWindowsWatchdog::duplicateProcessToken(HANDLE process, LPSECURITY_ATTRIBUTES security) +{ + HANDLE sourceToken; + + BOOL tokenRet = OpenProcessToken( + process, + TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, + &sourceToken); + + if (!tokenRet) { + LOG((CLOG_ERR "could not open token, process handle: %d", process)); + throw XArch(new XArchEvalWindows()); + } + + LOG((CLOG_DEBUG "got token %i, duplicating", sourceToken)); + + HANDLE newToken; + BOOL duplicateRet = DuplicateTokenEx( + sourceToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, security, + SecurityImpersonation, TokenPrimary, &newToken); + + if (!duplicateRet) { + LOG((CLOG_ERR "could not duplicate token %i", sourceToken)); + throw XArch(new XArchEvalWindows()); + } + + LOG((CLOG_DEBUG "duplicated, new token: %i", newToken)); + return newToken; +} + +HANDLE +MSWindowsWatchdog::getUserToken(LPSECURITY_ATTRIBUTES security) +{ + // 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 barrier 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) { + LOG((CLOG_DEBUG "getting elevated token, %s", + (m_elevateProcess ? "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); + } else { + LOG((CLOG_DEBUG "getting non-elevated token")); + return m_session.getUserToken(security); + } +} + +void +MSWindowsWatchdog::mainLoop(void*) +{ + shutdownExistingProcesses(); + + SendSas sendSasFunc = NULL; + HINSTANCE sasLib = LoadLibrary("sas.dll"); + if (sasLib) { + LOG((CLOG_DEBUG "found sas.dll")); + sendSasFunc = (SendSas)GetProcAddress(sasLib, "SendSAS"); + } + + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = NULL; + + if (!CreatePipe(&m_stdOutRead, &m_stdOutWrite, &saAttr, 0)) { + 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")); + 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)); + ARCH->sleep(timeout); + } + + if (!getCommand().empty() && ((m_processFailures != 0) || m_session.hasChanged() || m_commandChanged)) { + startProcess(); + } + + if (m_processRunning && !isProcessActive()) { + + m_processFailures++; + m_processRunning = false; + + LOG((CLOG_WARN "detected application not running, pid=%d", + m_processInfo.dwProcessId)); + } + + if (sendSasFunc != NULL) { + + HANDLE sendSasEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\SendSAS"); + if (sendSasEvent != NULL) { + + // use SendSAS event to wait for next session (timeout 1 second). + if (WaitForSingleObject(sendSasEvent, 1000) == WAIT_OBJECT_0) { + LOG((CLOG_DEBUG "calling SendSAS")); + sendSasFunc(FALSE); + } + + CloseHandle(sendSasEvent); + continue; + } + } + + // if the sas event failed, wait by sleeping. + ARCH->sleep(1); + + } + catch (std::exception& e) { + LOG((CLOG_ERR "failed to launch, error: %s", e.what())); + m_processFailures++; + m_processRunning = false; + continue; + } + catch (...) { + LOG((CLOG_ERR "failed to launch, unknown error.")); + m_processFailures++; + m_processRunning = false; + continue; + } + } + + if (m_processRunning) { + LOG((CLOG_DEBUG "terminated running process on exit")); + shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20); + } + + LOG((CLOG_DEBUG "watchdog main thread finished")); +} + +bool +MSWindowsWatchdog::isProcessActive() +{ + DWORD exitCode; + GetExitCodeProcess(m_processInfo.hProcess, &exitCode); + return exitCode == STILL_ACTIVE; +} + +void +MSWindowsWatchdog::setFileLogOutputter(FileLogOutputter* outputter) +{ + m_fileLogOutputter = outputter; +} + +void +MSWindowsWatchdog::startProcess() +{ + if (m_command.empty()) { + throw XMSWindowsWatchdogError("cannot start process, command is empty"); + } + + m_commandChanged = false; + + if (m_processRunning) { + LOG((CLOG_DEBUG "closing existing process to make way for new one")); + shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20); + m_processRunning = false; + } + + m_session.updateActiveSession(); + + BOOL createRet; + if (!m_daemonized) { + createRet = doStartProcessAsSelf(m_command); + } else { + SECURITY_ATTRIBUTES sa; + ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); + + getActiveDesktop(&sa); + + 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 + // http://symless.com/spit/issues/details/3338/#c70 + DWORD uiAccess = 1; + SetTokenInformation(userToken, TokenUIAccess, &uiAccess, sizeof(DWORD)); + + createRet = doStartProcessAsUser(m_command, userToken, &sa); + } + + if (!createRet) { + LOG((CLOG_ERR "could not launch")); + DWORD exitCode = 0; + GetExitCodeProcess(m_processInfo.hProcess, &exitCode); + LOG((CLOG_ERR "exit code: %d", exitCode)); + throw XArch(new XArchEvalWindows); + } + else { + // wait for program to fail. + ARCH->sleep(1); + if (!isProcessActive()) { + throw XMSWindowsWatchdogError("process immediately stopped"); + } + + m_processRunning = true; + m_processFailures = 0; + + LOG((CLOG_DEBUG "started process, session=%i, elevated: %s, command=%s", + m_session.getActiveSessionId(), + m_elevateProcess ? "yes" : "no", + m_command.c_str())); + } +} + +BOOL +MSWindowsWatchdog::doStartProcessAsSelf(String& command) +{ + DWORD creationFlags = + NORMAL_PRIORITY_CLASS | + CREATE_NO_WINDOW | + CREATE_UNICODE_ENVIRONMENT; + + STARTUPINFO si; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = "winsta0\\Default"; // TODO: maybe this should be \winlogon if we have logonui.exe? + si.hStdError = m_stdOutWrite; + si.hStdOutput = m_stdOutWrite; + si.dwFlags |= STARTF_USESTDHANDLES; + + LOG((CLOG_INFO "starting new process as self")); + return CreateProcess(NULL, LPSTR(command.c_str()), NULL, NULL, FALSE, creationFlags, NULL, NULL, &si, &m_processInfo); +} + +BOOL +MSWindowsWatchdog::doStartProcessAsUser(String& command, HANDLE userToken, LPSECURITY_ATTRIBUTES sa) +{ + // clear, as we're reusing process info struct + ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION)); + + STARTUPINFO si; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = "winsta0\\Default"; // TODO: maybe this should be \winlogon if we have logonui.exe? + si.hStdError = m_stdOutWrite; + si.hStdOutput = m_stdOutWrite; + si.dwFlags |= STARTF_USESTDHANDLES; + + 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 as privileged user")); + BOOL createRet = CreateProcessAsUser( + userToken, NULL, LPSTR(command.c_str()), + sa, NULL, TRUE, creationFlags, + environment, NULL, &si, &m_processInfo); + + DestroyEnvironmentBlock(environment); + CloseHandle(userToken); + + return createRet; +} + +void +MSWindowsWatchdog::setCommand(const std::string& command, bool elevate) +{ + LOG((CLOG_INFO "service command updated")); + m_command = command; + m_elevateProcess = elevate; + m_commandChanged = true; + m_processFailures = 0; +} + +std::string +MSWindowsWatchdog::getCommand() const +{ + if (!m_autoDetectCommand) { + return m_command; + } + + // seems like a fairly convoluted way to get the process name + const char* launchName = App::instance().argsBase().m_pname; + std::string args = ARCH->commandLine(); + + // build up a full command line + std::stringstream cmdTemp; + cmdTemp << launchName << args; + + std::string cmd = cmdTemp.str(); + + size_t i; + std::string find = "--relaunch"; + while ((i = cmd.find(find)) != std::string::npos) { + cmd.replace(i, find.length(), ""); + } + + return cmd; +} + +void +MSWindowsWatchdog::outputLoop(void*) +{ + // +1 char for \0 + CHAR buffer[kOutputBufferSize + 1]; + + while (m_monitoring) { + + DWORD bytesRead; + BOOL success = ReadFile(m_stdOutRead, buffer, kOutputBufferSize, &bytesRead, NULL); + + // assume the process has gone away? slow down + // the reads until another one turns up. + if (!success || bytesRead == 0) { + ARCH->sleep(1); + } + else { + buffer[bytesRead] = '\0'; + + testOutput(buffer); + + m_ipcLogOutputter.write(kINFO, buffer); + + if (m_fileLogOutputter != NULL) { + m_fileLogOutputter->write(kINFO, buffer); + } + } + } +} + +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, kIpcClientNode); + + // 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_INFO "process %d was shutdown gracefully", pid)); + break; + } + else { + + double elapsed = (ARCH->time() - start); + if (elapsed > timeout) { + // if timeout reached, kill forcefully. + // calling TerminateProcess on barrier is very bad! + // it causes the hook DLL to stay loaded in some apps, + // making it impossible to start barrier again. + LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", (int)elapsed)); + TerminateProcess(handle, kExitSuccess); + break; + } + + ARCH->sleep(1); + } + } +} + +void +MSWindowsWatchdog::shutdownExistingProcesses() +{ + // first we need to take a snapshot of the running processes + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + LOG((CLOG_ERR "could not get process snapshot")); + throw XArch(new XArchEvalWindows); + } + + PROCESSENTRY32 entry; + entry.dwSize = sizeof(PROCESSENTRY32); + + // get the first process, and if we can't do that then it's + // unlikely we can go any further + BOOL gotEntry = Process32First(snapshot, &entry); + if (!gotEntry) { + LOG((CLOG_ERR "could not get first process entry")); + throw XArch(new XArchEvalWindows); + } + + // now just iterate until we can find winlogon.exe pid + DWORD pid = 0; + while (gotEntry) { + + // make sure we're not checking the system process + if (entry.th32ProcessID != 0) { + + if (_stricmp(entry.szExeFile, "barrierc.exe") == 0 || + _stricmp(entry.szExeFile, "barriers.exe") == 0) { + + HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID); + shutdownProcess(handle, entry.th32ProcessID, 10); + } + } + + // now move on to the next entry (if we're not at the end) + gotEntry = Process32Next(snapshot, &entry); + if (!gotEntry) { + + DWORD err = GetLastError(); + 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")); + throw XArch(new XArchEvalWindows); + } + } + } + + CloseHandle(snapshot); + m_processRunning = false; +} + +void +MSWindowsWatchdog::getActiveDesktop(LPSECURITY_ATTRIBUTES security) +{ + String installedDir = ARCH->getInstalledDirectory(); + if (!installedDir.empty()) { + String syntoolCommand; + syntoolCommand.append("\"").append(installedDir).append("\\").append("syntool").append("\""); + syntoolCommand.append(" --get-active-desktop"); + + m_session.updateActiveSession(); + bool elevateProcess = m_elevateProcess; + m_elevateProcess = true; + HANDLE userToken = getUserToken(security); + m_elevateProcess = elevateProcess; + + BOOL createRet = doStartProcessAsUser(syntoolCommand, userToken, security); + + if (!createRet) { + DWORD rc = GetLastError(); + RevertToSelf(); + } + else { + LOG((CLOG_DEBUG "launched syntool to check active desktop")); + } + + ARCH->lockMutex(m_mutex); + int waitTime = 0; + while (!m_ready) { + if (waitTime >= MAXIMUM_WAIT_TIME) { + break; + } + + ARCH->waitCondVar(m_condVar, m_mutex, 1.0); + waitTime++; + } + m_ready = false; + ARCH->unlockMutex(m_mutex); + } +} + +void +MSWindowsWatchdog::testOutput(String buffer) +{ + // HACK: check standard output seems hacky. + size_t i = buffer.find(g_activeDesktop); + if (i != String::npos) { + size_t s = sizeof(g_activeDesktop); + String defaultDesktop("Default"); + String sub = buffer.substr(i + s - 1, defaultDesktop.size()); + if (sub != defaultDesktop) { + m_autoElevated = true; + } + + ARCH->lockMutex(m_mutex); + m_ready = true; + ARCH->broadcastCondVar(m_condVar); + ARCH->unlockMutex(m_mutex); + } +} |
