1 /*
2 * synergy -- mouse and keyboard sharing utility
3 * Copyright (C) 2012-2016 Symless Ltd.
4 * Copyright (C) 2009 Chris Schoeneman
5 *
6 * This package is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * found in the file LICENSE that should have accompanied this file.
9 *
10 * This package is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "platform/MSWindowsWatchdog.h"
20
21 #include "ipc/IpcLogOutputter.h"
22 #include "ipc/IpcServer.h"
23 #include "ipc/IpcMessage.h"
24 #include "ipc/Ipc.h"
25 #include "synergy/App.h"
26 #include "synergy/ArgsBase.h"
27 #include "mt/Thread.h"
28 #include "arch/win32/ArchDaemonWindows.h"
29 #include "arch/win32/XArchWindows.h"
30 #include "arch/Arch.h"
31 #include "base/log_outputters.h"
32 #include "base/TMethodJob.h"
33 #include "base/Log.h"
34 #include "common/Version.h"
35
36 #include <sstream>
37 #include <UserEnv.h>
38 #include <Shellapi.h>
39
40 #define CURRENT_PROCESS_ID 0
41 #define MAXIMUM_WAIT_TIME 3
42 enum {
43 kOutputBufferSize = 4096
44 };
45
46 typedef VOID (WINAPI *SendSas)(BOOL asUser);
47
48 const char g_activeDesktop[] = {"activeDesktop:"};
49
MSWindowsWatchdog(bool autoDetectCommand,IpcServer & ipcServer,IpcLogOutputter & ipcLogOutputter,bool foreground)50 MSWindowsWatchdog::MSWindowsWatchdog(
51 bool autoDetectCommand,
52 IpcServer& ipcServer,
53 IpcLogOutputter& ipcLogOutputter,
54 bool foreground) :
55 m_thread(NULL),
56 m_autoDetectCommand(autoDetectCommand),
57 m_monitoring(true),
58 m_commandChanged(false),
59 m_stdOutWrite(NULL),
60 m_stdOutRead(NULL),
61 m_ipcServer(ipcServer),
62 m_ipcLogOutputter(ipcLogOutputter),
63 m_elevateProcess(false),
64 m_processFailures(0),
65 m_processRunning(false),
66 m_fileLogOutputter(NULL),
67 m_autoElevated(false),
68 m_ready(false),
69 m_foreground(foreground)
70 {
71 m_mutex = ARCH->newMutex();
72 m_condVar = ARCH->newCondVar();
73 }
74
~MSWindowsWatchdog()75 MSWindowsWatchdog::~MSWindowsWatchdog()
76 {
77 if (m_condVar != NULL) {
78 ARCH->closeCondVar(m_condVar);
79 }
80
81 if (m_mutex != NULL) {
82 ARCH->closeMutex(m_mutex);
83 }
84 }
85
86 void
startAsync()87 MSWindowsWatchdog::startAsync()
88 {
89 m_thread = new Thread(new TMethodJob<MSWindowsWatchdog>(
90 this, &MSWindowsWatchdog::mainLoop, nullptr));
91
92 m_outputThread = new Thread(new TMethodJob<MSWindowsWatchdog>(
93 this, &MSWindowsWatchdog::outputLoop, nullptr));
94 }
95
96 void
stop()97 MSWindowsWatchdog::stop()
98 {
99 m_monitoring = false;
100
101 m_thread->wait(5);
102 delete m_thread;
103
104 m_outputThread->wait(5);
105 delete m_outputThread;
106 }
107
108 HANDLE
duplicateProcessToken(HANDLE process,LPSECURITY_ATTRIBUTES security)109 MSWindowsWatchdog::duplicateProcessToken(HANDLE process, LPSECURITY_ATTRIBUTES security)
110 {
111 HANDLE sourceToken;
112
113 BOOL tokenRet = OpenProcessToken(
114 process,
115 TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
116 &sourceToken);
117
118 if (!tokenRet) {
119 LOG((CLOG_ERR "could not open token, process handle: %d", process));
120 throw XArch(new XArchEvalWindows());
121 }
122
123 LOG((CLOG_DEBUG "got token %i, duplicating", sourceToken));
124
125 HANDLE newToken;
126 BOOL duplicateRet = DuplicateTokenEx(
127 sourceToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, security,
128 SecurityImpersonation, TokenPrimary, &newToken);
129
130 if (!duplicateRet) {
131 LOG((CLOG_ERR "could not duplicate token %i", sourceToken));
132 throw XArch(new XArchEvalWindows());
133 }
134
135 LOG((CLOG_DEBUG "duplicated, new token: %i", newToken));
136 return newToken;
137 }
138
139 HANDLE
getUserToken(LPSECURITY_ATTRIBUTES security)140 MSWindowsWatchdog::getUserToken(LPSECURITY_ATTRIBUTES security)
141 {
142 // always elevate if we are at the vista/7 login screen. we could also
143 // elevate for the uac dialog (consent.exe) but this would be pointless,
144 // since synergy would re-launch as non-elevated after the desk switch,
145 // and so would be unusable with the new elevated process taking focus.
146 if (m_elevateProcess
147 || m_autoElevated
148 || m_session.isProcessInSession("logonui.exe", NULL)) {
149
150 LOG((CLOG_DEBUG "getting elevated token, %s",
151 (m_elevateProcess ? "elevation required" : "at login screen")));
152
153 HANDLE process;
154 if (!m_session.isProcessInSession("winlogon.exe", &process)) {
155 throw XMSWindowsWatchdogError("cannot get user token without winlogon.exe");
156 }
157
158 return duplicateProcessToken(process, security);
159 }
160 else {
161 LOG((CLOG_DEBUG "getting non-elevated token"));
162 return m_session.getUserToken(security);
163 }
164 }
165
166 void
mainLoop(void *)167 MSWindowsWatchdog::mainLoop(void*)
168 {
169 shutdownExistingProcesses();
170
171 SendSas sendSasFunc = NULL;
172 HINSTANCE sasLib = LoadLibrary("sas.dll");
173 if (sasLib) {
174 LOG((CLOG_DEBUG "found sas.dll"));
175 sendSasFunc = (SendSas)GetProcAddress(sasLib, "SendSAS");
176 }
177
178 SECURITY_ATTRIBUTES saAttr;
179 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
180 saAttr.bInheritHandle = TRUE;
181 saAttr.lpSecurityDescriptor = NULL;
182
183 if (!CreatePipe(&m_stdOutRead, &m_stdOutWrite, &saAttr, 0)) {
184 throw XArch(new XArchEvalWindows());
185 }
186
187 ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION));
188
189 while (m_monitoring) {
190 try {
191
192 if (m_processRunning && getCommand().empty()) {
193 LOG((CLOG_INFO "process started but command is empty, shutting down"));
194 shutdownExistingProcesses();
195 m_processRunning = false;
196 continue;
197 }
198
199 if (m_processFailures != 0) {
200 // increasing backoff period, maximum of 10 seconds.
201 int timeout = (m_processFailures * 2) < 10 ? (m_processFailures * 2) : 10;
202 LOG((CLOG_INFO "backing off, wait=%ds, failures=%d", timeout, m_processFailures));
203 ARCH->sleep(timeout);
204 }
205
206 if (!getCommand().empty()) {
207 bool startNeeded = false;
208
209 if (m_processFailures != 0) {
210 startNeeded = true;
211 }
212 else if (!m_foreground && m_session.hasChanged()) {
213 startNeeded = true;
214 }
215 else if (m_commandChanged) {
216 startNeeded = true;
217 }
218
219 if (startNeeded) {
220 startProcess();
221 }
222 }
223
224 if (m_processRunning && !isProcessActive()) {
225
226 m_processFailures++;
227 m_processRunning = false;
228
229 LOG((CLOG_WARN "detected application not running, pid=%d",
230 m_processInfo.dwProcessId));
231 }
232
233 if (sendSasFunc != NULL) {
234
235 HANDLE sendSasEvent = CreateEvent(NULL, FALSE, FALSE, "Global\\SendSAS");
236 if (sendSasEvent != NULL) {
237
238 // use SendSAS event to wait for next session (timeout 1 second).
239 if (WaitForSingleObject(sendSasEvent, 1000) == WAIT_OBJECT_0) {
240 LOG((CLOG_DEBUG "calling SendSAS"));
241 sendSasFunc(FALSE);
242 }
243
244 CloseHandle(sendSasEvent);
245 continue;
246 }
247 }
248
249 // if the sas event failed, wait by sleeping.
250 ARCH->sleep(1);
251
252 }
253 catch (std::exception& e) {
254 LOG((CLOG_ERR "failed to launch, error: %s", e.what()));
255 m_processFailures++;
256 m_processRunning = false;
257 continue;
258 }
259 catch (...) {
260 LOG((CLOG_ERR "failed to launch, unknown error."));
261 m_processFailures++;
262 m_processRunning = false;
263 continue;
264 }
265 }
266
267 if (m_processRunning) {
268 LOG((CLOG_DEBUG "terminated running process on exit"));
269 shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20);
270 }
271
272 LOG((CLOG_DEBUG "watchdog main thread finished"));
273 }
274
275 bool
isProcessActive()276 MSWindowsWatchdog::isProcessActive()
277 {
278 DWORD exitCode;
279 GetExitCodeProcess(m_processInfo.hProcess, &exitCode);
280 return exitCode == STILL_ACTIVE;
281 }
282
283 void
setFileLogOutputter(FileLogOutputter * outputter)284 MSWindowsWatchdog::setFileLogOutputter(FileLogOutputter* outputter)
285 {
286 m_fileLogOutputter = outputter;
287 }
288
289 void
startProcess()290 MSWindowsWatchdog::startProcess()
291 {
292 if (m_command.empty()) {
293 throw XMSWindowsWatchdogError("cannot start process, command is empty");
294 }
295
296 m_commandChanged = false;
297
298 if (m_processRunning) {
299 LOG((CLOG_DEBUG "closing existing process to make way for new one"));
300 shutdownProcess(m_processInfo.hProcess, m_processInfo.dwProcessId, 20);
301 m_processRunning = false;
302 }
303
304 BOOL createRet;
305 if (m_foreground) {
306 LOG((CLOG_DEBUG "starting command in foreground"));
307 createRet = startProcessInForeground(m_command);
308 }
309 else {
310 LOG((CLOG_DEBUG "starting command as session user"));
311 m_session.updateActiveSession();
312
313 SECURITY_ATTRIBUTES sa;
314 ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));
315
316 getActiveDesktop(&sa);
317
318 ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));
319 HANDLE userToken = getUserToken(&sa);
320 m_elevateProcess = m_autoElevated ? m_autoElevated : m_elevateProcess;
321 m_autoElevated = false;
322
323 // patch by Jack Zhou and Henry Tung
324 // set UIAccess to fix Windows 8 GUI interaction
325 DWORD uiAccess = 1;
326 SetTokenInformation(userToken, TokenUIAccess, &uiAccess, sizeof(DWORD));
327
328 createRet = startProcessAsUser(m_command, userToken, &sa);
329 }
330
331 if (!createRet) {
332 LOG((CLOG_ERR "could not launch command"));
333 DWORD exitCode = 0;
334 GetExitCodeProcess(m_processInfo.hProcess, &exitCode);
335 LOG((CLOG_ERR "exit code: %d", exitCode));
336 throw XArch(new XArchEvalWindows);
337 }
338 else {
339 // wait for program to fail.
340 ARCH->sleep(1);
341 if (!isProcessActive()) {
342 closeProcessHandles(m_processInfo.dwProcessId);
343 throw XMSWindowsWatchdogError("process immediately stopped");
344 }
345
346 m_processRunning = true;
347 m_processFailures = 0;
348
349 LOG((CLOG_DEBUG "started process, session=%i, elevated: %s, command=%s",
350 m_session.getActiveSessionId(),
351 m_elevateProcess ? "yes" : "no",
352 m_command.c_str()));
353 }
354 }
355
356 void
setStartupInfo(STARTUPINFO & si)357 MSWindowsWatchdog::setStartupInfo(STARTUPINFO& si)
358 {
359 ZeroMemory(&si, sizeof(STARTUPINFO));
360 si.cb = sizeof(STARTUPINFO);
361 si.lpDesktop = "winsta0\\Default"; // TODO: maybe this should be \winlogon if we have logonui.exe?
362 si.hStdError = m_stdOutWrite;
363 si.hStdOutput = m_stdOutWrite;
364 si.dwFlags |= STARTF_USESTDHANDLES;
365 }
366
367 BOOL
startProcessInForeground(String & command)368 MSWindowsWatchdog::startProcessInForeground(String& command)
369 {
370 // clear, as we're reusing process info struct
371 ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION));
372
373 // show the console window when in foreground mode,
374 // so we can close it gracefully, but minimize it
375 // so it doesn't get in the way.
376 STARTUPINFO si;
377 setStartupInfo(si);
378 si.dwFlags |= STARTF_USESHOWWINDOW;
379 si.wShowWindow = SW_MINIMIZE;
380
381 BOOL result = CreateProcess(
382 NULL, LPSTR(command.c_str()), NULL, NULL,
383 TRUE, 0, NULL, NULL, &si, &m_processInfo);
384
385 m_children.insert(std::make_pair(m_processInfo.dwProcessId, m_processInfo));
386
387 return result;
388 }
389
390 BOOL
startProcessAsUser(String & command,HANDLE userToken,LPSECURITY_ATTRIBUTES sa)391 MSWindowsWatchdog::startProcessAsUser(String& command, HANDLE userToken, LPSECURITY_ATTRIBUTES sa)
392 {
393 // clear, as we're reusing process info struct
394 ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION));
395
396 STARTUPINFO si;
397 setStartupInfo(si);
398
399 LPVOID environment;
400 BOOL blockRet = CreateEnvironmentBlock(&environment, userToken, FALSE);
401 if (!blockRet) {
402 LOG((CLOG_ERR "could not create environment block"));
403 throw XArch(new XArchEvalWindows);
404 }
405
406 DWORD creationFlags =
407 NORMAL_PRIORITY_CLASS |
408 CREATE_NO_WINDOW |
409 CREATE_UNICODE_ENVIRONMENT;
410
411 // re-launch in current active user session
412 LOG((CLOG_INFO "starting new process"));
413 BOOL createRet = CreateProcessAsUser(
414 userToken, NULL, LPSTR(command.c_str()),
415 sa, NULL, TRUE, creationFlags,
416 environment, NULL, &si, &m_processInfo);
417
418 m_children.insert(std::make_pair(m_processInfo.dwProcessId, m_processInfo));
419
420 DestroyEnvironmentBlock(environment);
421 CloseHandle(userToken);
422
423 return createRet;
424 }
425
426 void
setCommand(const std::string & command,bool elevate)427 MSWindowsWatchdog::setCommand(const std::string& command, bool elevate)
428 {
429 LOG((CLOG_INFO "service command updated"));
430 m_command = command;
431 m_elevateProcess = elevate;
432 m_commandChanged = true;
433 m_processFailures = 0;
434 }
435
436 std::string
getCommand() const437 MSWindowsWatchdog::getCommand() const
438 {
439 if (!m_autoDetectCommand) {
440 return m_command;
441 }
442
443 // seems like a fairly convoluted way to get the process name
444 const char* launchName = App::instance().argsBase().m_pname;
445 std::string args = ARCH->commandLine();
446
447 // build up a full command line
448 std::stringstream cmdTemp;
449 cmdTemp << launchName << args;
450
451 std::string cmd = cmdTemp.str();
452
453 size_t i;
454 std::string find = "--relaunch";
455 while ((i = cmd.find(find)) != std::string::npos) {
456 cmd.replace(i, find.length(), "");
457 }
458
459 return cmd;
460 }
461
462 void
outputLoop(void *)463 MSWindowsWatchdog::outputLoop(void*)
464 {
465 // +1 char for \0
466 CHAR buffer[kOutputBufferSize + 1];
467
468 while (m_monitoring) {
469
470 DWORD bytesRead;
471 BOOL success = ReadFile(m_stdOutRead, buffer, kOutputBufferSize, &bytesRead, NULL);
472
473 // assume the process has gone away? slow down
474 // the reads until another one turns up.
475 if (!success || bytesRead == 0) {
476 ARCH->sleep(1);
477 }
478 else {
479 buffer[bytesRead] = '\0';
480
481 testOutput(buffer);
482
483 m_ipcLogOutputter.write(kINFO, buffer);
484
485 if (m_fileLogOutputter != NULL) {
486 m_fileLogOutputter->write(kINFO, buffer);
487 }
488
489 #if SYSAPI_WIN32
490 if (m_foreground) {
491 // when in foreground mode (useful for debugging), send the core
492 // process output to the VS debug output window.
493 // we could use the MSWindowsDebugOutputter, but it's really fiddly to so,
494 // and there doesn't seem to be an advantage of doing that.
495 OutputDebugString(buffer);
496 }
497 #endif
498 }
499 }
500 }
501
502 void
shutdownProcess(HANDLE handle,DWORD pid,int timeout)503 MSWindowsWatchdog::shutdownProcess(HANDLE handle, DWORD pid, int timeout)
504 {
505 DWORD exitCode;
506 GetExitCodeProcess(handle, &exitCode);
507 if (exitCode != STILL_ACTIVE) {
508 return;
509 }
510
511 IpcShutdownMessage shutdown;
512 m_ipcServer.send(shutdown, kIpcClientNode);
513
514 // wait for process to exit gracefully.
515 double start = ARCH->time();
516 while (true) {
517
518 GetExitCodeProcess(handle, &exitCode);
519 if (exitCode != STILL_ACTIVE) {
520 // yay, we got a graceful shutdown. there should be no hook in use errors!
521 LOG((CLOG_DEBUG "process %d was shutdown gracefully", pid));
522 break;
523 }
524 else {
525
526 double elapsed = (ARCH->time() - start);
527 if (elapsed > timeout) {
528 // if timeout reached, kill forcefully.
529 // calling TerminateProcess on synergy is very bad!
530 // it causes the hook DLL to stay loaded in some apps,
531 // making it impossible to start synergy again.
532 LOG((CLOG_WARN "shutdown timed out after %d secs, forcefully terminating", (int)elapsed));
533 TerminateProcess(handle, kExitSuccess);
534 break;
535 }
536
537 ARCH->sleep(1);
538 }
539 }
540
541 closeProcessHandles(pid);
542 }
543
544 void
shutdownExistingProcesses()545 MSWindowsWatchdog::shutdownExistingProcesses()
546 {
547 // first we need to take a snapshot of the running processes
548 HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, CURRENT_PROCESS_ID);
549 if (snapshot == INVALID_HANDLE_VALUE) {
550 LOG((CLOG_ERR "could not get process snapshot"));
551 throw XArch(new XArchEvalWindows);
552 }
553
554 PROCESSENTRY32 entry;
555 entry.dwSize = sizeof(PROCESSENTRY32);
556
557 // get the first process, and if we can't do that then it's
558 // unlikely we can go any further
559 BOOL gotEntry = Process32First(snapshot, &entry);
560 if (!gotEntry) {
561 LOG((CLOG_ERR "could not get first process entry"));
562 throw XArch(new XArchEvalWindows);
563 }
564
565 // now just iterate until we can find winlogon.exe pid
566 DWORD pid = 0;
567 while (gotEntry) {
568
569 // make sure we're not checking the system process
570 if (entry.th32ProcessID != 0) {
571
572 if (_stricmp(entry.szExeFile, "synergyc.exe") == 0 ||
573 _stricmp(entry.szExeFile, "synergys.exe") == 0) {
574
575 HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID);
576 shutdownProcess(handle, entry.th32ProcessID, 10);
577 CloseHandle(handle);
578 }
579 }
580
581 // now move on to the next entry (if we're not at the end)
582 gotEntry = Process32Next(snapshot, &entry);
583 if (!gotEntry) {
584
585 DWORD err = GetLastError();
586 if (err != ERROR_NO_MORE_FILES) {
587
588 // only worry about error if it's not the end of the snapshot
589 LOG((CLOG_ERR "could not get subsiquent process entry"));
590 throw XArch(new XArchEvalWindows);
591 }
592 }
593 }
594
595 clearAllChildren();
596 CloseHandle(snapshot);
597 m_processRunning = false;
598 }
599
600 void
getActiveDesktop(LPSECURITY_ATTRIBUTES security)601 MSWindowsWatchdog::getActiveDesktop(LPSECURITY_ATTRIBUTES security)
602 {
603 String installedDir = ARCH->getInstalledDirectory();
604 if (!installedDir.empty()) {
605 String syntoolCommand;
606 syntoolCommand.append("\"").append(installedDir).append("\\").append("syntool").append("\"");
607 syntoolCommand.append(" --get-active-desktop");
608
609 m_session.updateActiveSession();
610 bool elevateProcess = m_elevateProcess;
611 m_elevateProcess = true;
612 HANDLE userToken = getUserToken(security);
613 m_elevateProcess = elevateProcess;
614
615 BOOL createRet = startProcessAsUser(syntoolCommand, userToken, security);
616 auto pid = m_processInfo.dwProcessId;
617 if (!createRet) {
618 DWORD rc = GetLastError();
619 RevertToSelf();
620 }
621 else {
622 LOG((CLOG_DEBUG "launched syntool to check active desktop"));
623 }
624
625 ARCH->lockMutex(m_mutex);
626 int waitTime = 0;
627 while (!m_ready) {
628 if (waitTime >= MAXIMUM_WAIT_TIME) {
629 break;
630 }
631
632 ARCH->waitCondVar(m_condVar, m_mutex, 1.0);
633 waitTime++;
634 }
635 m_ready = false;
636 ARCH->unlockMutex(m_mutex);
637 closeProcessHandles(pid);
638 }
639 }
640
641 void
testOutput(String buffer)642 MSWindowsWatchdog::testOutput(String buffer)
643 {
644 // HACK: check standard output seems hacky.
645 size_t i = buffer.find(g_activeDesktop);
646 if (i != String::npos) {
647 size_t s = sizeof(g_activeDesktop);
648 String defaultDesktop("Default");
649 String sub = buffer.substr(i + s - 1, defaultDesktop.size());
650 if (sub != defaultDesktop) {
651 m_autoElevated = true;
652 }
653
654 ARCH->lockMutex(m_mutex);
655 m_ready = true;
656 ARCH->broadcastCondVar(m_condVar);
657 ARCH->unlockMutex(m_mutex);
658 }
659 }
660
closeProcessHandles(unsigned long pid,bool removeFromMap)661 void MSWindowsWatchdog::closeProcessHandles(unsigned long pid, bool removeFromMap) {
662 auto processInfo = m_children.find(pid);
663 if (processInfo != m_children.end()) {
664 CloseHandle(processInfo->second.hProcess);
665 CloseHandle(processInfo->second.hThread);
666 if(removeFromMap) {
667 m_children.erase(processInfo);
668 }
669 }
670 }
671
clearAllChildren()672 void MSWindowsWatchdog::clearAllChildren() {
673 for (auto it = m_children.begin(); it != m_children.end(); ++it)
674 {
675 closeProcessHandles(it->second.dwThreadId, false);
676 }
677 m_children.clear();
678 }
679