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