diff --git a/cmake/CMakeLists_config.txt b/cmake/CMakeLists_config.txt index 441604ce..66a26e21 100644 --- a/cmake/CMakeLists_config.txt +++ b/cmake/CMakeLists_config.txt @@ -182,6 +182,8 @@ IF(UNIX) ENDIF(APPLE) ELSE(UNIX) + + LIST(APPEND libs Wtsapi32 Userenv) ADD_DEFINITIONS( /DWIN32 diff --git a/cmake/CMakeLists_lib.txt b/cmake/CMakeLists_lib.txt index 7fb53de3..a6972100 100644 --- a/cmake/CMakeLists_lib.txt +++ b/cmake/CMakeLists_lib.txt @@ -204,6 +204,7 @@ SET(src_lib_platform_mswindows ${root_lib}/platform/CMSWindowsScreen.cpp ${root_lib}/platform/CMSWindowsScreenSaver.cpp ${root_lib}/platform/CMSWindowsUtil.cpp + ${root_lib}/platform/CMSWindowsRelauncher.cpp ) SET(inc_lib_platform_mswindows @@ -219,6 +220,7 @@ SET(inc_lib_platform_mswindows ${root_lib}/platform/CMSWindowsScreen.h ${root_lib}/platform/CMSWindowsScreenSaver.h ${root_lib}/platform/CMSWindowsUtil.h + ${root_lib}/platform/CMSWindowsRelauncher.h ) SET(src_lib_platform_hook diff --git a/lib/arch/CArch.h b/lib/arch/CArch.h index 9eda5863..a5822812 100644 --- a/lib/arch/CArch.h +++ b/lib/arch/CArch.h @@ -54,8 +54,7 @@ class CArch : public IArchConsole, public IArchString, public IArchSystem, public IArchTaskBar, - public IArchTime, - public IArchAppUtil { + public IArchTime { public: CArch(); ~CArch(); @@ -190,6 +189,10 @@ public: virtual int run(int argc, char** argv, CreateTaskBarReceiverFunc createTaskBarReceiver); virtual void beforeAppExit(); + // expose util so we don't need to re-implement all the functions + IArchAppUtil& util() const { return *m_appUtil; } + IArchDaemon& daemon() const { return *m_daemon; } + private: static CArch* s_instance; diff --git a/lib/arch/CArchAppUtilWindows.cpp b/lib/arch/CArchAppUtilWindows.cpp index e04315a6..5e01caa2 100644 --- a/lib/arch/CArchAppUtilWindows.cpp +++ b/lib/arch/CArchAppUtilWindows.cpp @@ -22,6 +22,8 @@ #include "CMSWindowsScreen.h" #include "XSynergy.h" #include "IArchTaskBarReceiver.h" +#include "CMSWindowsRelauncher.h" +#include "CScreen.h" #include #include @@ -79,6 +81,9 @@ CArchAppUtilWindows::parseArg(const int& argc, const char* const* argv, int& i) else if (app().isArg(i, argc, argv, NULL, "--debug-service-wait")) { app().argsBase().m_debugServiceWait = true; + } + else if (app().isArg(i, argc, argv, NULL, "--relaunch")) { + app().argsBase().m_relaunchMode = true; } else { // option not supported here @@ -187,7 +192,7 @@ CArchAppUtilWindows::stopService() } } - LOG((CLOG_INFO "service '%s' stopping asyncronously", app().daemonName())); + LOG((CLOG_INFO "service '%s' stopping asynchronously", app().daemonName())); } static @@ -200,10 +205,12 @@ mainLoopStatic() int CArchAppUtilWindows::daemonNTMainLoop(int argc, const char** argv) { - app().initialize(argc, argv); + app().initApp(argc, argv); debugServiceWait(); + + // NB: what the hell does this do?! app().argsBase().m_backend = false; - app().loadConfig(); + return CArchMiscWindows::runDaemon(mainLoopStatic); } @@ -305,3 +312,21 @@ CArchAppUtilWindows::debugServiceWait() } } } + +void +CArchAppUtilWindows::startNode() +{ + if (app().argsBase().m_relaunchMode) { + + LOG((CLOG_DEBUG1 "entering relaunch mode")); + CMSWindowsRelauncher relauncher; + relauncher.startAsync(); + + // HACK: create a dummy screen, which can handle system events + // (such as a stop request from the service controller). + CScreen* dummyScreen = app().createScreen(); + } + else { + app().startNode(); + } +} diff --git a/lib/arch/CArchAppUtilWindows.h b/lib/arch/CArchAppUtilWindows.h index 0f1c2a24..505a6526 100644 --- a/lib/arch/CArchAppUtilWindows.h +++ b/lib/arch/CArchAppUtilWindows.h @@ -66,6 +66,8 @@ public: static CArchAppUtilWindows& instance(); + void startNode(); + private: AppExitMode m_exitMode; static BOOL WINAPI consoleHandler(DWORD CEvent); diff --git a/lib/arch/CArchDaemonWindows.cpp b/lib/arch/CArchDaemonWindows.cpp index 4d3f04ff..ee24ac99 100644 --- a/lib/arch/CArchDaemonWindows.cpp +++ b/lib/arch/CArchDaemonWindows.cpp @@ -615,13 +615,14 @@ CArchDaemonWindows::serviceMain(DWORD argc, LPTSTR* argvIn) m_serviceState = SERVICE_START_PENDING; setStatus(m_serviceState, 0, 10000); + std::string commandLine; + // if no arguments supplied then try getting them from the registry. // the first argument doesn't count because it's the service name. Arguments args; ArgList myArgv; if (argc <= 1) { // read command line - std::string commandLine; HKEY key = openNTServicesKey(); key = CArchMiscWindows::openKey(key, argvIn[0]); key = CArchMiscWindows::openKey(key, _T("Parameters")); @@ -685,6 +686,8 @@ CArchDaemonWindows::serviceMain(DWORD argc, LPTSTR* argvIn) } } + m_commandLine = commandLine; + try { // invoke daemon function m_daemonResult = m_daemonFunc(static_cast(argc), argv); diff --git a/lib/arch/CArchDaemonWindows.h b/lib/arch/CArchDaemonWindows.h index ed09fab9..22ed26ba 100644 --- a/lib/arch/CArchDaemonWindows.h +++ b/lib/arch/CArchDaemonWindows.h @@ -83,6 +83,8 @@ public: virtual bool canInstallDaemon(const char* name, bool allUsers); virtual bool isDaemonInstalled(const char* name, bool allUsers); + std::string commandLine() const { return m_commandLine; } + private: static HKEY openNTServicesKey(); static HKEY open95ServicesKey(); @@ -129,6 +131,8 @@ private: SERVICE_STATUS_HANDLE m_statusHandle; UINT m_quitMessage; + + std::string m_commandLine; }; #endif diff --git a/lib/arch/IArchAppUtil.h b/lib/arch/IArchAppUtil.h index a43220c7..f2c88a65 100644 --- a/lib/arch/IArchAppUtil.h +++ b/lib/arch/IArchAppUtil.h @@ -27,4 +27,5 @@ public: virtual CApp& app() const = 0; virtual int run(int argc, char** argv, CreateTaskBarReceiverFunc createTaskBarReceiver) = 0; virtual void beforeAppExit() = 0; + virtual void startNode() = 0; }; diff --git a/lib/platform/CMSWindowsRelauncher.cpp b/lib/platform/CMSWindowsRelauncher.cpp new file mode 100644 index 00000000..61991b19 --- /dev/null +++ b/lib/platform/CMSWindowsRelauncher.cpp @@ -0,0 +1,309 @@ +#include "CMSWindowsRelauncher.h" +#include "CThread.h" +#include "TMethodJob.h" +#include "CLog.h" +#include "CArch.h" +#include "Version.h" +#include "CArchDaemonWindows.h" + +#include +#include +#include + +CMSWindowsRelauncher::CMSWindowsRelauncher() +{ + +} + +CMSWindowsRelauncher::~CMSWindowsRelauncher() +{ + delete m_thread; +} + +void +CMSWindowsRelauncher::startAsync() +{ + m_thread = new CThread(new TMethodJob( + this, &CMSWindowsRelauncher::startThread, nullptr)); +} + +void +CMSWindowsRelauncher::startThread(void*) +{ + LOG((CLOG_DEBUG "starting relaunch loop")); + int ret = relaunchLoop(); + + // HACK: this actually throws an exception to exit with 0 (nasty) + ARCH->util().app().m_bye(ret); +} + +// this still gets the physical session (the one the keyboard and +// mouse is connected to), sometimes this returns -1 but not sure why +DWORD +CMSWindowsRelauncher::getSessionId() +{ + return WTSGetActiveConsoleSessionId(); +} + +BOOL +CMSWindowsRelauncher::winlogonInSession(DWORD sessionId, PHANDLE process) +{ + // 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 (error: %i)", + GetLastError())); + return 0; + } + + 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 (error: %i)", + GetLastError())); + return 0; + } + + // used to record process names for debug info + std::list nameList; + + // 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) { + + DWORD processSessionId; + BOOL pidToSidRet = ProcessIdToSessionId( + entry.th32ProcessID, &processSessionId); + + if (!pidToSidRet) { + LOG((CLOG_ERR "could not get session id for process id %i (error: %i)", + entry.th32ProcessID, GetLastError())); + return 0; + } + + // only pay attention to processes in the active session + if (processSessionId == sessionId) { + + // store the names so we can record them for debug + nameList.push_back(entry.szExeFile); + + if (_stricmp(entry.szExeFile, "winlogon.exe") == 0) { + pid = entry.th32ProcessID; + break; + } + } + } + + // 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 (error: %i)", + GetLastError())); + return 0; + } + } + } + + std::string nameListJoin; + for(std::list::iterator it = nameList.begin(); + it != nameList.end(); it++) { + nameListJoin.append(*it); + nameListJoin.append(", "); + } + + LOG((CLOG_DEBUG "checked processes while looking for winlogon.exe: %s", + nameListJoin.c_str())); + + CloseHandle(snapshot); + + if (pid) { + // now get the process so we can get the process, with which + // we'll use to get the process token. + *process = OpenProcess(MAXIMUM_ALLOWED, FALSE, pid); + return true; + } + else { + LOG((CLOG_DEBUG "could not find winlogon.exe in session %i", sessionId)); + return false; + } +} + +// gets the current user (so we can launch under their session) +HANDLE +CMSWindowsRelauncher::getCurrentUserToken(DWORD sessionId, LPSECURITY_ATTRIBUTES security) +{ + HANDLE currentToken; + HANDLE winlogonProcess; + + if (winlogonInSession(sessionId, &winlogonProcess)) { + + LOG((CLOG_DEBUG "session %i has winlogon.exe", sessionId)); + + // get the token, so we can re-launch with this token + // -- do we really need all these access bits? + BOOL tokenRet = OpenProcessToken( + winlogonProcess, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | + TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | + TOKEN_ADJUST_SESSIONID | TOKEN_READ | TOKEN_WRITE, + ¤tToken); + } + else { + + LOG((CLOG_ERR "session %i does not have winlogon.exe " + "which is needed for re-launch", sessionId)); + return 0; + } + + HANDLE primaryToken; + BOOL duplicateRet = DuplicateTokenEx( + currentToken, MAXIMUM_ALLOWED, security, + SecurityImpersonation, TokenPrimary, &primaryToken); + + if (!duplicateRet) { + LOG((CLOG_ERR "could not duplicate token %i (error: %i)", + currentToken, GetLastError())); + return 0; + } + + return primaryToken; +} + +int +CMSWindowsRelauncher::relaunchLoop() +{ + // start with invalid id (gets re-assigned on first loop) + DWORD sessionId = -1; + + // keep here so we can check if proc running -- huh? + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + int returnCode = kExitSuccess; + bool launched = false; + + // TODO: fix this hack BEFORE release; we need to exit gracefully instead + // of being force killed! + bool loopRunning = true; + while (loopRunning) { + + DWORD newSessionId = getSessionId(); + + // only enter here when id changes, and the session isn't -1, which + // may mean that there is no active session. + if ((newSessionId != sessionId) && (newSessionId != -1)) { + + // HACK: doesn't close process in a nice way + // TODO: use CloseMainWindow instead + if (launched) { + TerminateProcess(pi.hProcess, kExitSuccess); + LOG((CLOG_DEBUG "terminated existing process to make way for new one")); + launched = false; + } + + // ok, this is now the active session (forget the old one if any) + sessionId = newSessionId; + + SECURITY_ATTRIBUTES sa; + ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES)); + + // get the token for the user in active session, which is the + // one receiving input from mouse and keyboard. + HANDLE userToken = getCurrentUserToken(sessionId, &sa); + + if (userToken != 0) { + LOG((CLOG_DEBUG "got user token to launch new process")); + + std::string cmd = getCommand(); + + // in case reusing process info struct + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + + STARTUPINFO si; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = "winsta0\\default"; + + LPVOID environment; + BOOL blockRet = CreateEnvironmentBlock(&environment, userToken, FALSE); + if (!blockRet) { + LOG((CLOG_ERR "could not create environment block (error: %i)", + GetLastError())); + + returnCode = kExitFailed; + loopRunning = false; // stop loop + } + else { + + DWORD creationFlags = + NORMAL_PRIORITY_CLASS | + CREATE_NO_WINDOW | + CREATE_UNICODE_ENVIRONMENT; + + // re-launch in current active user session + BOOL createRet = CreateProcessAsUser( + userToken, NULL, LPSTR(cmd.c_str()), + &sa, NULL, TRUE, creationFlags, + environment, NULL, &si, &pi); + + DestroyEnvironmentBlock(environment); + CloseHandle(userToken); + + if (!createRet) { + LOG((CLOG_ERR "could not launch (error: %i)", GetLastError())); + returnCode = kExitFailed; + loopRunning = false; + } + else { + LOG((CLOG_DEBUG "launched in session %i (cmd: %s)", + sessionId, cmd.c_str())); + launched = true; + } + } + } + } + + // check for session change every second + ARCH->sleep(1); + } + + if (launched) { + // HACK: kill just in case process it has survived somehow + TerminateProcess(pi.hProcess, kExitSuccess); + } + + return kExitSuccess; +} + +std::string CMSWindowsRelauncher::getCommand() +{ + // seems like a fairly convoluted way to get the process name + const char* launchName = ARCH->util().app().argsBase().m_pname; + std::string args = ((CArchDaemonWindows&)ARCH->daemon()).commandLine(); + + // build up a full command line + std::stringstream cmdTemp; + cmdTemp << launchName << /*" --debug-data session-" << sessionId <<*/ 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; +} diff --git a/lib/platform/CMSWindowsRelauncher.h b/lib/platform/CMSWindowsRelauncher.h new file mode 100644 index 00000000..895ef2b9 --- /dev/null +++ b/lib/platform/CMSWindowsRelauncher.h @@ -0,0 +1,21 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include + +class CThread; + +class CMSWindowsRelauncher { +public: + CMSWindowsRelauncher(); + virtual ~CMSWindowsRelauncher(); + void startAsync(); + CThread* m_thread; + void startThread(void*); + BOOL winlogonInSession(DWORD sessionId, PHANDLE process); + DWORD getSessionId(); + HANDLE getCurrentUserToken(DWORD sessionId, LPSECURITY_ATTRIBUTES security); + int relaunchLoop(); + std::string getCommand(); +}; diff --git a/lib/synergy/CApp.cpp b/lib/synergy/CApp.cpp index cbc4c512..d6b01258 100644 --- a/lib/synergy/CApp.cpp +++ b/lib/synergy/CApp.cpp @@ -49,6 +49,7 @@ CApp::CArgsBase::CArgsBase() : #if SYSAPI_WIN32 m_daemon(false), // daemon mode not supported on windows (use --service) m_debugServiceWait(false), +m_relaunchMode(false), #else m_daemon(true), // backward compatibility for unix (daemon by default) #endif @@ -311,7 +312,7 @@ CApp::loggingFilterWarning() } void -CApp::initialize(int argc, const char** argv) +CApp::initApp(int argc, const char** argv) { // parse command line parseArgs(argc, argv); diff --git a/lib/synergy/CApp.h b/lib/synergy/CApp.h index 44fe6648..d42373c4 100644 --- a/lib/synergy/CApp.h +++ b/lib/synergy/CApp.h @@ -21,6 +21,7 @@ class IArchTaskBarReceiver; class CBufferedLogOutputter; class ILogOutputter; class CFileLogOutputter; +class CScreen; typedef IArchTaskBarReceiver* (*CreateTaskBarReceiverFunc)(const CBufferedLogOutputter*); typedef int (*StartupFunc)(int, char**); @@ -41,6 +42,7 @@ public: const char* m_display; CString m_name; #if SYSAPI_WIN32 + bool m_relaunchMode; bool m_debugServiceWait; #endif }; @@ -98,10 +100,15 @@ public: void loggingFilterWarning(); // Parses args, sets up file logging, and loads the config. - void initialize(int argc, const char** argv); + void initApp(int argc, const char** argv); // HACK: accept non-const, but make it const anyway - void initialize(int argc, char** argv) { initialize(argc, (const char**)argv); } + void initApp(int argc, char** argv) { initApp(argc, (const char**)argv); } + + // Start the server or client. + virtual void startNode() = 0; + + virtual CScreen* createScreen() = 0; protected: virtual void parseArgs(int argc, const char* const* argv, int &i); @@ -157,6 +164,8 @@ private: " [--service ]" # define HELP_SYS_INFO \ " --service manage the windows service, valid options are:\n" \ - " install/uninstall/start/stop\n" + " install/uninstall/start/stop\n" \ + " --relaunch persistently relaunches process in current user \n" \ + " session (useful for vista and upward).\n" #endif diff --git a/lib/synergy/CClientApp.cpp b/lib/synergy/CClientApp.cpp index f468ffa7..62cc6092 100644 --- a/lib/synergy/CClientApp.cpp +++ b/lib/synergy/CClientApp.cpp @@ -412,7 +412,7 @@ CClientApp::closeClient(CClient* client) int CClientApp::foregroundStartup(int argc, char** argv) { - initialize(argc, argv); + initApp(argc, argv); // never daemonize return mainLoop(); @@ -483,12 +483,8 @@ CClientApp::mainLoop() // create the event queue CEventQueue eventQueue; - // start the client. if this return false then we've failed and - // we shouldn't retry. - LOG((CLOG_DEBUG1 "starting client")); - if (!startClient()) { - return kExitFailed; - } + // start client, etc + ARCH->util().startNode(); // run event loop. if startClient() failed we're supposed to retry // later. the timer installed by startClient() will take care of @@ -522,7 +518,7 @@ daemonMainLoopStatic(int argc, const char** argv) int CClientApp::standardStartup(int argc, char** argv) { - initialize(argc, argv); + initApp(argc, argv); // daemonize if requested if (args().m_daemon) { @@ -572,3 +568,14 @@ CClientApp::runInner(int argc, char** argv, ILogOutputter* outputter, StartupFun return result; } + +void +CClientApp::startNode() +{ + // start the client. if this return false then we've failed and + // we shouldn't retry. + LOG((CLOG_DEBUG1 "starting client")); + if (!startClient()) { + m_bye(kExitFailed); + } +} diff --git a/lib/synergy/CClientApp.h b/lib/synergy/CClientApp.h index 39c67bce..be9b4b89 100644 --- a/lib/synergy/CClientApp.h +++ b/lib/synergy/CClientApp.h @@ -73,6 +73,7 @@ public: bool startClient(); void stopClient(); int mainLoop(); + void startNode(); static CClientApp& instance() { return (CClientApp&)CApp::instance(); } diff --git a/lib/synergy/CServerApp.cpp b/lib/synergy/CServerApp.cpp index f39e10ab..b92ff07b 100644 --- a/lib/synergy/CServerApp.cpp +++ b/lib/synergy/CServerApp.cpp @@ -740,12 +740,8 @@ int CServerApp::mainLoop() return kExitFailed; } - // start the server. if this return false then we've failed and - // we shouldn't retry. - LOG((CLOG_DEBUG1 "starting server")); - if (!startServer()) { - return kExitFailed; - } + // start server, etc + ARCH->util().startNode(); // handle hangup signal by reloading the server's configuration ARCH->setSignalHandler(CArch::kHANGUP, &reloadSignalHandler, NULL); @@ -839,7 +835,7 @@ int daemonMainLoopStatic(int argc, const char** argv) { int CServerApp::standardStartup(int argc, char** argv) { - initialize(argc, argv); + initApp(argc, argv); // daemonize if requested if (args().m_daemon) { @@ -853,7 +849,7 @@ CServerApp::standardStartup(int argc, char** argv) int CServerApp::foregroundStartup(int argc, char** argv) { - initialize(argc, argv); + initApp(argc, argv); // never daemonize return mainLoop(); @@ -886,3 +882,13 @@ CServerApp::daemonInfo() const #endif } +void +CServerApp::startNode() +{ + // start the server. if this return false then we've failed and + // we shouldn't retry. + LOG((CLOG_DEBUG1 "starting server")); + if (!startServer()) { + m_bye(kExitFailed); + } +} diff --git a/lib/synergy/CServerApp.h b/lib/synergy/CServerApp.h index 2dda6ff7..c2ea50dd 100644 --- a/lib/synergy/CServerApp.h +++ b/lib/synergy/CServerApp.h @@ -102,6 +102,7 @@ public: int runInner(int argc, char** argv, ILogOutputter* outputter, StartupFunc startup, CreateTaskBarReceiverFunc createTaskBarReceiver); int standardStartup(int argc, char** argv); int foregroundStartup(int argc, char** argv); + void startNode(); static CServerApp& instance() { return (CServerApp&)CApp::instance(); }