• Main Page
  • Classes
  • Files
  • File List

CMSWindowsRelauncher.cpp

00001 /*
00002  * synergy -- mouse and keyboard sharing utility
00003  * Copyright (C) 2012 Bolton Software Ltd.
00004  * Copyright (C) 2009 Chris Schoeneman
00005  *
00006  * This package is free software; you can redistribute it and/or
00007  * modify it under the terms of the GNU General Public License
00008  * found in the file COPYING that should have accompanied this file.
00009  * 
00010  * This package is distributed in the hope that it will be useful,
00011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013  * GNU General Public License for more details.
00014  *
00015  * You should have received a copy of the GNU General Public License
00016  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
00017  */
00018 
00019 #include "CMSWindowsRelauncher.h"
00020 #include "CThread.h"
00021 #include "TMethodJob.h"
00022 #include "CLog.h"
00023 #include "CArch.h"
00024 #include "Version.h"
00025 #include "CArchDaemonWindows.h"
00026 #include "XArchWindows.h"
00027 #include "CApp.h"
00028 #include "CArgsBase.h"
00029 #include "CIpcLogOutputter.h"
00030 #include "CIpcServer.h"
00031 #include "CIpcMessage.h"
00032 #include "Ipc.h"
00033 
00034 #include <Tlhelp32.h>
00035 #include <UserEnv.h>
00036 #include <sstream>
00037 #include <Wtsapi32.h>
00038 #include <Shellapi.h>
00039 
00040 enum {
00041     kOutputBufferSize = 4096
00042 };
00043 
00044 typedef VOID (WINAPI *SendSas)(BOOL asUser);
00045 
00046 CMSWindowsRelauncher::CMSWindowsRelauncher(
00047     bool autoDetectCommand,
00048     CIpcServer& ipcServer,
00049     CIpcLogOutputter& ipcLogOutputter) :
00050     m_thread(NULL),
00051     m_autoDetectCommand(autoDetectCommand),
00052     m_running(true),
00053     m_commandChanged(false),
00054     m_stdOutWrite(NULL),
00055     m_stdOutRead(NULL),
00056     m_ipcServer(ipcServer),
00057     m_ipcLogOutputter(ipcLogOutputter),
00058     m_elevateProcess(false)
00059 {
00060 }
00061 
00062 CMSWindowsRelauncher::~CMSWindowsRelauncher()
00063 {
00064 }
00065 
00066 void 
00067 CMSWindowsRelauncher::startAsync()
00068 {
00069     m_thread = new CThread(new TMethodJob<CMSWindowsRelauncher>(
00070         this, &CMSWindowsRelauncher::mainLoop, nullptr));
00071 
00072     m_outputThread = new CThread(new TMethodJob<CMSWindowsRelauncher>(
00073         this, &CMSWindowsRelauncher::outputLoop, nullptr));
00074 }
00075 
00076 void
00077 CMSWindowsRelauncher::stop()
00078 {
00079     m_running = false;
00080     
00081     m_thread->wait(5);
00082     delete m_thread;
00083 
00084     m_outputThread->wait(5);
00085     delete m_outputThread;
00086 }
00087 
00088 // this still gets the physical session (the one the keyboard and 
00089 // mouse is connected to), sometimes this returns -1 but not sure why
00090 DWORD 
00091 CMSWindowsRelauncher::getSessionId()
00092 {
00093     return WTSGetActiveConsoleSessionId();
00094 }
00095 
00096 BOOL
00097 CMSWindowsRelauncher::isProcessInSession(const char* name, DWORD sessionId, PHANDLE process = NULL)
00098 {
00099     // first we need to take a snapshot of the running processes
00100     HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
00101     if (snapshot == INVALID_HANDLE_VALUE) {
00102         LOG((CLOG_ERR "could not get process snapshot (error: %i)", 
00103             GetLastError()));
00104         return 0;
00105     }
00106 
00107     PROCESSENTRY32 entry;
00108     entry.dwSize = sizeof(PROCESSENTRY32);
00109 
00110     // get the first process, and if we can't do that then it's 
00111     // unlikely we can go any further
00112     BOOL gotEntry = Process32First(snapshot, &entry);
00113     if (!gotEntry) {
00114         LOG((CLOG_ERR "could not get first process entry (error: %i)", 
00115             GetLastError()));
00116         return 0;
00117     }
00118 
00119     // used to record process names for debug info
00120     std::list<std::string> nameList;
00121 
00122     // now just iterate until we can find winlogon.exe pid
00123     DWORD pid = 0;
00124     while(gotEntry) {
00125 
00126         // make sure we're not checking the system process
00127         if (entry.th32ProcessID != 0) {
00128 
00129             DWORD processSessionId;
00130             BOOL pidToSidRet = ProcessIdToSessionId(
00131                 entry.th32ProcessID, &processSessionId);
00132 
00133             if (!pidToSidRet) {
00134                 LOG((CLOG_ERR "could not get session id for process id %i (error: %i)",
00135                     entry.th32ProcessID, GetLastError()));
00136                 return 0;
00137             }
00138 
00139             // only pay attention to processes in the active session
00140             if (processSessionId == sessionId) {
00141 
00142                 // store the names so we can record them for debug
00143                 nameList.push_back(entry.szExeFile);
00144 
00145                 if (_stricmp(entry.szExeFile, name) == 0) {
00146                     pid = entry.th32ProcessID;
00147                 }
00148             }
00149         }
00150 
00151         // now move on to the next entry (if we're not at the end)
00152         gotEntry = Process32Next(snapshot, &entry);
00153         if (!gotEntry) {
00154 
00155             DWORD err = GetLastError();
00156             if (err != ERROR_NO_MORE_FILES) {
00157 
00158                 // only worry about error if it's not the end of the snapshot
00159                 LOG((CLOG_ERR "could not get subsiquent process entry (error: %i)", 
00160                     GetLastError()));
00161                 return 0;
00162             }
00163         }
00164     }
00165 
00166     std::string nameListJoin;
00167     for(std::list<std::string>::iterator it = nameList.begin();
00168         it != nameList.end(); it++) {
00169             nameListJoin.append(*it);
00170             nameListJoin.append(", ");
00171     }
00172 
00173     LOG((CLOG_DEBUG "processes in session %d: %s",
00174         sessionId, nameListJoin.c_str()));
00175 
00176     CloseHandle(snapshot);
00177 
00178     if (pid) {
00179         if (process != NULL) {
00180             // now get the process so we can get the process, with which
00181             // we'll use to get the process token.
00182             LOG((CLOG_DEBUG "found %s in session %i", name, sessionId));
00183             *process = OpenProcess(MAXIMUM_ALLOWED, FALSE, pid);
00184         }
00185         return true;
00186     }
00187     else {
00188         return false;
00189     }
00190 }
00191 
00192 HANDLE
00193 CMSWindowsRelauncher::duplicateProcessToken(HANDLE process, LPSECURITY_ATTRIBUTES security)
00194 {
00195     HANDLE sourceToken;
00196 
00197     BOOL tokenRet = OpenProcessToken(
00198         process,
00199         TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
00200         &sourceToken);
00201 
00202     if (!tokenRet) {
00203         LOG((CLOG_ERR "could not open token, process handle: %d (error: %i)", process, GetLastError()));
00204         return NULL;
00205     }
00206     
00207     LOG((CLOG_DEBUG "got token %i, duplicating", sourceToken));
00208 
00209     HANDLE newToken;
00210     BOOL duplicateRet = DuplicateTokenEx(
00211         sourceToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, security,
00212         SecurityImpersonation, TokenPrimary, &newToken);
00213 
00214     if (!duplicateRet) {
00215         LOG((CLOG_ERR "could not duplicate token %i (error: %i)",
00216             sourceToken, GetLastError()));
00217         return NULL;
00218     }
00219     
00220     LOG((CLOG_DEBUG "duplicated, new token: %i", newToken));
00221     return newToken;
00222 }
00223 
00224 HANDLE 
00225 CMSWindowsRelauncher::getUserToken(DWORD sessionId, LPSECURITY_ATTRIBUTES security)
00226 {
00227     // always elevate if we are at the vista/7 login screen. we could also 
00228     // elevate for the uac dialog (consent.exe) but this would be pointless,
00229     // since synergy would re-launch as non-elevated after the desk switch,
00230     // and so would be unusable with the new elevated process taking focus.
00231     if (m_elevateProcess || isProcessInSession("logonui.exe", sessionId)) {
00232         
00233         LOG((CLOG_DEBUG "getting elevated token, %s",
00234             (m_elevateProcess ? "elevation required" : "at login screen")));
00235 
00236         HANDLE process;
00237         if (isProcessInSession("winlogon.exe", sessionId, &process)) {
00238             return duplicateProcessToken(process, security);
00239         }
00240         else {
00241             LOG((CLOG_ERR "could not find winlogon in session %i", sessionId));
00242             return NULL;
00243         }
00244     }
00245     else {
00246         LOG((CLOG_DEBUG "getting non-elevated token"));
00247         return getSessionToken(sessionId, security);
00248     }
00249 }
00250 
00251 HANDLE 
00252 CMSWindowsRelauncher::getSessionToken(DWORD sessionId, LPSECURITY_ATTRIBUTES security)
00253 {
00254     HANDLE sourceToken;
00255     if (!WTSQueryUserToken(sessionId, &sourceToken)) {
00256         LOG((CLOG_ERR "could not get token from session %d (error: %i)", sessionId, GetLastError()));
00257         return 0;
00258     }
00259     
00260     HANDLE newToken;
00261     if (!DuplicateTokenEx(
00262         sourceToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, security,
00263         SecurityImpersonation, TokenPrimary, &newToken)) {
00264 
00265         LOG((CLOG_ERR "could not duplicate token (error: %i)", GetLastError()));
00266         return 0;
00267     }
00268     
00269     LOG((CLOG_DEBUG "duplicated, new token: %i", newToken));
00270     return newToken;
00271 }
00272 
00273 void
00274 CMSWindowsRelauncher::mainLoop(void*)
00275 {
00276     shutdownExistingProcesses();
00277 
00278     SendSas sendSasFunc = NULL;
00279     HINSTANCE sasLib = LoadLibrary("sas.dll");
00280     if (sasLib) {
00281         LOG((CLOG_DEBUG "found sas.dll"));
00282         sendSasFunc = (SendSas)GetProcAddress(sasLib, "SendSAS");
00283     }
00284 
00285     DWORD sessionId = -1;
00286     bool launched = false;
00287 
00288     SECURITY_ATTRIBUTES saAttr; 
00289     saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); 
00290     saAttr.bInheritHandle = TRUE; 
00291     saAttr.lpSecurityDescriptor = NULL; 
00292 
00293     if (!CreatePipe(&m_stdOutRead, &m_stdOutWrite, &saAttr, 0)) {
00294         throw XArch(new XArchEvalWindows());
00295     }
00296 
00297     PROCESS_INFORMATION pi;
00298     ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
00299 
00300     int failures = 0;
00301 
00302     while (m_running) {
00303 
00304         HANDLE sendSasEvent = 0;
00305         if (sasLib && sendSasFunc) {
00306             // can't we just create one event? seems weird creating a new
00307             // event every second...
00308             sendSasEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\SendSAS");
00309         }
00310 
00311         DWORD newSessionId = getSessionId();
00312 
00313         bool running = false;
00314         if (launched) {
00315 
00316             DWORD exitCode;
00317             GetExitCodeProcess(pi.hProcess, &exitCode);
00318             running = (exitCode == STILL_ACTIVE);
00319 
00320             if (!running) {
00321                 failures++;
00322                 LOG((CLOG_INFO "detected application not running, pid=%d, failures=%d", pi.dwProcessId, failures));
00323                 
00324                 // increasing backoff period, maximum of 10 seconds.
00325                 int timeout = (failures * 2) < 10 ? (failures * 2) : 10;
00326                 LOG((CLOG_DEBUG "waiting, backoff period is %d seconds", timeout));
00327                 ARCH->sleep(timeout);
00328                 
00329                 // double check, in case process started after we waited.
00330                 GetExitCodeProcess(pi.hProcess, &exitCode);
00331                 running = (exitCode == STILL_ACTIVE);
00332             }
00333             else {
00334                 // reset failures when running.
00335                 failures = 0;
00336             }
00337         }
00338 
00339         // only enter here when id changes, and the session isn't -1, which
00340         // may mean that there is no active session.
00341         bool sessionChanged = ((newSessionId != sessionId) && (newSessionId != -1));
00342 
00343         // relaunch if it was running but has stopped unexpectedly.
00344         bool stoppedRunning = (launched && !running);
00345 
00346         if (stoppedRunning || sessionChanged || m_commandChanged) {
00347             
00348             m_commandChanged = false;
00349 
00350             if (launched) {
00351                 LOG((CLOG_DEBUG "closing existing process to make way for new one"));
00352                 shutdownProcess(pi.hProcess, pi.dwProcessId, 20);
00353                 launched = false;
00354             }
00355 
00356             // ok, this is now the active session (forget the old one if any)
00357             sessionId = newSessionId;
00358 
00359             SECURITY_ATTRIBUTES sa;
00360             ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));
00361 
00362             HANDLE userToken = getUserToken(sessionId, &sa);
00363             if (userToken == NULL) {
00364                 // HACK: trigger retry mechanism.
00365                 launched = true;
00366                 continue;
00367             }
00368 
00369             std::string cmd = command();
00370             if (cmd == "") {
00371                 // this appears on first launch when the user hasn't configured
00372                 // anything yet, so don't show it as a warning, only show it as
00373                 // debug to devs to let them know why nothing happened.
00374                 LOG((CLOG_DEBUG "nothing to launch, no command specified."));
00375                 continue;
00376             }
00377 
00378             // in case reusing process info struct
00379             ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
00380 
00381             STARTUPINFO si;
00382             ZeroMemory(&si, sizeof(STARTUPINFO));
00383             si.cb = sizeof(STARTUPINFO);
00384             si.lpDesktop = "winsta0\\Default"; // TODO: maybe this should be \winlogon if we have logonui.exe?
00385             si.hStdError = m_stdOutWrite;
00386             si.hStdOutput = m_stdOutWrite;
00387             si.dwFlags |= STARTF_USESTDHANDLES;
00388 
00389             LPVOID environment;
00390             BOOL blockRet = CreateEnvironmentBlock(&environment, userToken, FALSE);
00391             if (!blockRet) {
00392                 LOG((CLOG_ERR "could not create environment block (error: %i)", 
00393                     GetLastError()));
00394                 continue;
00395             }
00396 
00397             DWORD creationFlags = 
00398                 NORMAL_PRIORITY_CLASS |
00399                 CREATE_NO_WINDOW |
00400                 CREATE_UNICODE_ENVIRONMENT;
00401 
00402             // re-launch in current active user session
00403             LOG((CLOG_INFO "starting new process"));
00404             BOOL createRet = CreateProcessAsUser(
00405                 userToken, NULL, LPSTR(cmd.c_str()),
00406                 &sa, NULL, TRUE, creationFlags,
00407                 environment, NULL, &si, &pi);
00408 
00409             DestroyEnvironmentBlock(environment);
00410             CloseHandle(userToken);
00411 
00412             if (!createRet) {
00413                 LOG((CLOG_ERR "could not launch (error: %i)", GetLastError()));
00414                 continue;
00415             }
00416             else {
00417                 LOG((CLOG_DEBUG "launched in session %i (cmd: %s)", 
00418                     sessionId, cmd.c_str()));
00419                 launched = true;
00420             }
00421         }
00422 
00423         if (sendSasEvent) {
00424             // use SendSAS event to wait for next session.
00425             if (WaitForSingleObject(sendSasEvent, 1000) == WAIT_OBJECT_0 && sendSasFunc) {
00426                 LOG((CLOG_DEBUG "calling SendSAS"));
00427                 sendSasFunc(FALSE);
00428             }
00429             CloseHandle(sendSasEvent);
00430         }
00431         else {
00432             // check for session change every second.
00433             ARCH->sleep(1);
00434         }
00435     }
00436 
00437     if (launched) {
00438         LOG((CLOG_DEBUG "terminated running process on exit"));
00439         shutdownProcess(pi.hProcess, pi.dwProcessId, 20);
00440     }
00441     
00442     LOG((CLOG_DEBUG "relauncher main thread finished"));
00443 }
00444 
00445 void
00446 CMSWindowsRelauncher::command(const std::string& command, bool elevate)
00447 {
00448     LOG((CLOG_INFO "service command updated"));
00449     m_command = command;
00450     m_elevateProcess = elevate;
00451     m_commandChanged = true;
00452 }
00453 
00454 std::string
00455 CMSWindowsRelauncher::command() const
00456 {
00457     if (!m_autoDetectCommand) {
00458         return m_command;
00459     }
00460 
00461     // seems like a fairly convoluted way to get the process name
00462     const char* launchName = CApp::instance().argsBase().m_pname;
00463     std::string args = ARCH->commandLine();
00464 
00465     // build up a full command line
00466     std::stringstream cmdTemp;
00467     cmdTemp << launchName << args;
00468 
00469     std::string cmd = cmdTemp.str();
00470 
00471     size_t i;
00472     std::string find = "--relaunch";
00473     while((i = cmd.find(find)) != std::string::npos) {
00474         cmd.replace(i, find.length(), "");
00475     }
00476 
00477     return cmd;
00478 }
00479 
00480 void
00481 CMSWindowsRelauncher::outputLoop(void*)
00482 {
00483     // +1 char for \0
00484     CHAR buffer[kOutputBufferSize + 1];
00485 
00486     while (m_running) {
00487         
00488         DWORD bytesRead;
00489         BOOL success = ReadFile(m_stdOutRead, buffer, kOutputBufferSize, &bytesRead, NULL);
00490 
00491         // assume the process has gone away? slow down
00492         // the reads until another one turns up.
00493         if (!success || bytesRead == 0) {
00494             ARCH->sleep(1);
00495         }
00496         else {
00497             buffer[bytesRead] = '\0';
00498 
00499             // send process output over IPC to GUI, and force it to be sent
00500             // which bypasses the ipc logging anti-recursion mechanism.
00501             m_ipcLogOutputter.write(kINFO, buffer, true);
00502         }
00503             
00504     }
00505 }
00506 
00507 void
00508 CMSWindowsRelauncher::shutdownProcess(HANDLE handle, DWORD pid, int timeout)
00509 {
00510     DWORD exitCode;
00511     GetExitCodeProcess(handle, &exitCode);
00512     if (exitCode != STILL_ACTIVE)
00513         return;
00514 
00515     CIpcShutdownMessage shutdown;
00516     m_ipcServer.send(shutdown, kIpcClientNode);
00517 
00518     // wait for process to exit gracefully.
00519     double start = ARCH->time();
00520     while (true)
00521     {
00522         GetExitCodeProcess(handle, &exitCode);
00523         if (exitCode != STILL_ACTIVE) {
00524             // yay, we got a graceful shutdown. there should be no hook in use errors!
00525             LOG((CLOG_INFO "process %d was shutdown gracefully", pid));
00526             break;
00527         }
00528         else {
00529             
00530             double elapsed = (ARCH->time() - start);
00531             if (elapsed > timeout) {
00532                 // if timeout reached, kill forcefully.
00533                 // calling TerminateProcess on synergy is very bad!
00534                 // it causes the hook DLL to stay loaded in some apps,
00535                 // making it impossible to start synergy again.
00536                 LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", (int)elapsed));
00537                 TerminateProcess(handle, kExitSuccess);
00538                 break;
00539             }
00540 
00541             ARCH->sleep(1);
00542         }
00543     }
00544 }
00545 
00546 void
00547 CMSWindowsRelauncher::shutdownExistingProcesses()
00548 {
00549     // first we need to take a snapshot of the running processes
00550     HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
00551     if (snapshot == INVALID_HANDLE_VALUE) {
00552         LOG((CLOG_ERR "could not get process snapshot (error: %i)", 
00553             GetLastError()));
00554         return;
00555     }
00556 
00557     PROCESSENTRY32 entry;
00558     entry.dwSize = sizeof(PROCESSENTRY32);
00559 
00560     // get the first process, and if we can't do that then it's 
00561     // unlikely we can go any further
00562     BOOL gotEntry = Process32First(snapshot, &entry);
00563     if (!gotEntry) {
00564         LOG((CLOG_ERR "could not get first process entry (error: %i)", 
00565             GetLastError()));
00566         return;
00567     }
00568 
00569     // now just iterate until we can find winlogon.exe pid
00570     DWORD pid = 0;
00571     while(gotEntry) {
00572 
00573         // make sure we're not checking the system process
00574         if (entry.th32ProcessID != 0) {
00575 
00576             if (_stricmp(entry.szExeFile, "synergyc.exe") == 0 ||
00577                 _stricmp(entry.szExeFile, "synergys.exe") == 0) {
00578                 
00579                 HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID);
00580                 shutdownProcess(handle, entry.th32ProcessID, 10);
00581             }
00582         }
00583 
00584         // now move on to the next entry (if we're not at the end)
00585         gotEntry = Process32Next(snapshot, &entry);
00586         if (!gotEntry) {
00587 
00588             DWORD err = GetLastError();
00589             if (err != ERROR_NO_MORE_FILES) {
00590 
00591                 // only worry about error if it's not the end of the snapshot
00592                 LOG((CLOG_ERR "could not get subsiquent process entry (error: %i)", 
00593                     GetLastError()));
00594                 return;
00595             }
00596         }
00597     }
00598 
00599     CloseHandle(snapshot);
00600 }

Generated on Sun May 19 2013 00:00:04 for Synergy by  doxygen 1.7.1