aboutsummaryrefslogtreecommitdiff
path: root/src/ca/CloseApps.cpp
diff options
context:
space:
mode:
authorSean Hall <r.sean.hall@gmail.com>2018-12-15 21:46:30 -0600
committerGitHub <noreply@github.com>2018-12-15 21:46:30 -0600
commitf7020c0d16baf2b960e7123e233e20c519f6a340 (patch)
treed2cd464ee15b2b3f304ff780c531b39bb292d331 /src/ca/CloseApps.cpp
parent6ed8d107e6edf16956c778bda3573f8d7a7690fc (diff)
downloadwix-f7020c0d16baf2b960e7123e233e20c519f6a340.tar.gz
wix-f7020c0d16baf2b960e7123e233e20c519f6a340.tar.bz2
wix-f7020c0d16baf2b960e7123e233e20c519f6a340.zip
Import implementation of UtilCA from old repo's WixCA/scasched/scaexec. (#3)
Diffstat (limited to 'src/ca/CloseApps.cpp')
-rw-r--r--src/ca/CloseApps.cpp570
1 files changed, 570 insertions, 0 deletions
diff --git a/src/ca/CloseApps.cpp b/src/ca/CloseApps.cpp
new file mode 100644
index 00000000..a3f28ed3
--- /dev/null
+++ b/src/ca/CloseApps.cpp
@@ -0,0 +1,570 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3#include "precomp.h"
4
5#define DEFAULT_PROCESS_EXIT_WAIT_TIME 5000
6
7// WixCloseApplication Target Description Condition Attributes Sequence
8
9// structs
10LPCWSTR wzQUERY_CLOSEAPPS = L"SELECT `WixCloseApplication`, `Target`, `Description`, `Condition`, `Attributes`, `Property`, `TerminateExitCode`, `Timeout` FROM `WixCloseApplication` ORDER BY `Sequence`";
11enum eQUERY_CLOSEAPPS { QCA_ID = 1, QCA_TARGET, QCA_DESCRIPTION, QCA_CONDITION, QCA_ATTRIBUTES, QCA_PROPERTY, QCA_TERMINATEEXITCODE, QCA_TIMEOUT };
12
13// CloseApplication.Attributes
14enum CLOSEAPP_ATTRIBUTES
15{
16 CLOSEAPP_ATTRIBUTE_NONE = 0x0,
17 CLOSEAPP_ATTRIBUTE_CLOSEMESSAGE = 0x1,
18 CLOSEAPP_ATTRIBUTE_REBOOTPROMPT = 0x2,
19 CLOSEAPP_ATTRIBUTE_ELEVATEDCLOSEMESSAGE = 0x4,
20 CLOSEAPP_ATTRIBUTE_ENDSESSIONMESSAGE = 0x8,
21 CLOSEAPP_ATTRIBUTE_ELEVATEDENDSESSIONMESSAGE = 0x10,
22 CLOSEAPP_ATTRIBUTE_TERMINATEPROCESS = 0x20,
23 CLOSEAPP_ATTRIBUTE_PROMPTTOCONTINUE = 0x40,
24};
25
26struct PROCESS_AND_MESSAGE
27{
28 DWORD dwProcessId;
29 DWORD dwMessageId;
30 DWORD dwTimeout;
31};
32
33
34/******************************************************************
35 EnumWindowsProc - callback function which sends message if the
36 current window matches the passed in process ID
37
38******************************************************************/
39BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
40{
41 PROCESS_AND_MESSAGE* pPM = reinterpret_cast<PROCESS_AND_MESSAGE*>(lParam);
42 DWORD dwProcessId = 0;
43 DWORD_PTR dwResult = 0;
44 BOOL fQueryEndSession = WM_QUERYENDSESSION == pPM->dwMessageId;
45 BOOL fContinueWindowsInProcess = TRUE; // assume we will send message to all top-level windows in a process.
46
47 ::GetWindowThreadProcessId(hwnd, &dwProcessId);
48
49 // check if the process Id is the one we're looking for
50 if (dwProcessId != pPM->dwProcessId)
51 {
52 return TRUE;
53 }
54
55 WcaLog(LOGMSG_VERBOSE, "Sending message to process id 0x%x", dwProcessId);
56
57 if (::SendMessageTimeoutW(hwnd, pPM->dwMessageId, 0, fQueryEndSession ? ENDSESSION_CLOSEAPP : 0, SMTO_BLOCK, pPM->dwTimeout, &dwResult))
58 {
59 WcaLog(LOGMSG_VERBOSE, "Result 0x%x", dwResult);
60
61 if (fQueryEndSession)
62 {
63 // If application said it was okay to close, do that.
64 if (dwResult)
65 {
66 ::SendMessageTimeoutW(hwnd, WM_ENDSESSION, TRUE, ENDSESSION_CLOSEAPP, SMTO_BLOCK, pPM->dwTimeout, &dwResult);
67 }
68 else // application said don't try to close it, so don't bother sending messages to any other top-level windows.
69 {
70 fContinueWindowsInProcess = FALSE;
71 }
72 }
73 }
74 else // log result message.
75 {
76 WcaLog(LOGMSG_VERBOSE, "Failed to send message id: %u, error: 0x%x", pPM->dwMessageId, ::GetLastError());
77 }
78
79 // so we know we succeeded
80 ::SetLastError(ERROR_SUCCESS);
81
82 return fContinueWindowsInProcess;
83}
84
85/******************************************************************
86 PromptToContinue - displays the prompt if the application is still
87 running.
88
89******************************************************************/
90static HRESULT PromptToContinue(
91 __in_z LPCWSTR wzApplication,
92 __in_z LPCWSTR wzPrompt
93 )
94{
95 HRESULT hr = S_OK;
96 UINT er = ERROR_SUCCESS;
97 PMSIHANDLE hRecMessage = NULL;
98 DWORD *prgProcessIds = NULL;
99 DWORD cProcessIds = 0;
100
101 hRecMessage = ::MsiCreateRecord(1);
102 ExitOnNull(hRecMessage, hr, E_OUTOFMEMORY, "Failed to create record for prompt.");
103
104 er = ::MsiRecordSetStringW(hRecMessage, 0, wzPrompt);
105 ExitOnWin32Error(er, hr, "Failed to set prompt record field string");
106
107 do
108 {
109 hr = ProcFindAllIdsFromExeName(wzApplication, &prgProcessIds, &cProcessIds);
110 if (SUCCEEDED(hr) && 0 < cProcessIds)
111 {
112 er = WcaProcessMessage(static_cast<INSTALLMESSAGE>(INSTALLMESSAGE_WARNING | MB_ABORTRETRYIGNORE | MB_DEFBUTTON3 | MB_ICONWARNING), hRecMessage);
113 if (IDABORT == er)
114 {
115 hr = HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT);
116 }
117 else if (IDRETRY == er)
118 {
119 hr = S_FALSE;
120 }
121 else if (IDIGNORE == er)
122 {
123 hr = S_OK;
124 }
125 else
126 {
127 ExitOnWin32Error(er, hr, "Unexpected return value from prompt to continue.");
128 }
129 }
130
131 ReleaseNullMem(prgProcessIds);
132 cProcessIds = 0;
133 } while (S_FALSE == hr);
134
135LExit:
136 ReleaseMem(prgProcessIds);
137 return hr;
138}
139
140/******************************************************************
141 SendProcessMessage - helper function to enumerate the top-level
142 windows and send to all matching a process ID.
143
144******************************************************************/
145void SendProcessMessage(
146 __in DWORD dwProcessId,
147 __in DWORD dwMessageId,
148 __in DWORD dwTimeout
149 )
150{
151 WcaLog(LOGMSG_VERBOSE, "Attempting to send process id 0x%x message id: %u", dwProcessId, dwMessageId);
152
153 PROCESS_AND_MESSAGE pm = { };
154 pm.dwProcessId = dwProcessId;
155 pm.dwMessageId = dwMessageId;
156 pm.dwTimeout = dwTimeout;
157
158 if (!::EnumWindows(EnumWindowsProc, reinterpret_cast<LPARAM>(&pm)))
159 {
160 DWORD dwLastError = ::GetLastError();
161 if (ERROR_SUCCESS != dwLastError)
162 {
163 WcaLog(LOGMSG_VERBOSE, "CloseApp enumeration error: 0x%x", dwLastError);
164 }
165 }
166}
167
168/******************************************************************
169 SendApplicationMessage - helper function to iterate through the
170 processes for the specified application and send all
171 applicable process Ids a message and give them time to process
172 the message.
173
174******************************************************************/
175void SendApplicationMessage(
176 __in LPCWSTR wzApplication,
177 __in DWORD dwMessageId,
178 __in DWORD dwTimeout
179 )
180{
181 DWORD *prgProcessIds = NULL;
182 DWORD cProcessIds = 0, iProcessId;
183 HRESULT hr = S_OK;
184
185 WcaLog(LOGMSG_VERBOSE, "Checking App: %ls ", wzApplication);
186
187 hr = ProcFindAllIdsFromExeName(wzApplication, &prgProcessIds, &cProcessIds);
188
189 if (SUCCEEDED(hr) && 0 < cProcessIds)
190 {
191 WcaLog(LOGMSG_VERBOSE, "App: %ls found running, %d processes, attempting to send message.", wzApplication, cProcessIds);
192
193 for (iProcessId = 0; iProcessId < cProcessIds; ++iProcessId)
194 {
195 SendProcessMessage(prgProcessIds[iProcessId], dwMessageId, dwTimeout);
196 }
197
198 ProcWaitForIds(prgProcessIds, cProcessIds, dwTimeout);
199 }
200
201 ReleaseMem(prgProcessIds);
202}
203
204/******************************************************************
205 SetRunningProcessProperty - helper function that sets the specified
206 property if there are any instances of the specified executable
207 running. Useful to show custom UI to ask for shutdown.
208******************************************************************/
209void SetRunningProcessProperty(
210 __in LPCWSTR wzApplication,
211 __in LPCWSTR wzProperty
212 )
213{
214 DWORD *prgProcessIds = NULL;
215 DWORD cProcessIds = 0;
216 HRESULT hr = S_OK;
217
218 WcaLog(LOGMSG_VERBOSE, "Checking App: %ls ", wzApplication);
219
220 hr = ProcFindAllIdsFromExeName(wzApplication, &prgProcessIds, &cProcessIds);
221
222 if (SUCCEEDED(hr) && 0 < cProcessIds)
223 {
224 WcaLog(LOGMSG_VERBOSE, "App: %ls found running, %d processes, setting '%ls' property.", wzApplication, cProcessIds, wzProperty);
225 WcaSetIntProperty(wzProperty, cProcessIds);
226 }
227
228 ReleaseMem(prgProcessIds);
229}
230
231/******************************************************************
232 TerminateProcesses - helper function that kills the provided set of
233 process ids such that they return a particular exit code.
234******************************************************************/
235void TerminateProcesses(
236 __in_ecount(cProcessIds) DWORD rgdwProcessIds[],
237 __in DWORD cProcessIds,
238 __in DWORD dwExitCode
239 )
240{
241 for (DWORD i = 0; i < cProcessIds; ++i)
242 {
243 HANDLE hProcess = ::OpenProcess(PROCESS_TERMINATE, FALSE, rgdwProcessIds[i]);
244 if (hProcess)
245 {
246 ::TerminateProcess(hProcess, dwExitCode);
247 ::CloseHandle(hProcess);
248 }
249 }
250}
251
252/******************************************************************
253 WixCloseApplications - entry point for WixCloseApplications Custom Action
254
255 called as Type 1 CustomAction (binary DLL) from Windows Installer
256 in InstallExecuteSequence before InstallFiles
257******************************************************************/
258extern "C" UINT __stdcall WixCloseApplications(
259 __in MSIHANDLE hInstall
260 )
261{
262 //AssertSz(FALSE, "debug WixCloseApplications");
263 HRESULT hr = S_OK;
264 UINT er = ERROR_SUCCESS;
265
266 LPWSTR pwzData = NULL;
267 LPWSTR pwzId = NULL;
268 LPWSTR pwzTarget = NULL;
269 LPWSTR pwzDescription = NULL;
270 LPWSTR pwzCondition = NULL;
271 LPWSTR pwzProperty = NULL;
272 DWORD dwAttributes = 0;
273 DWORD dwTimeout = 0;
274 DWORD dwTerminateExitCode = 0;
275 MSICONDITION condition = MSICONDITION_NONE;
276
277 DWORD cCloseApps = 0;
278
279 PMSIHANDLE hView = NULL;
280 PMSIHANDLE hRec = NULL;
281 MSIHANDLE hListboxTable = NULL;
282 MSIHANDLE hListboxColumns = NULL;
283
284 LPWSTR pwzCustomActionData = NULL;
285 //DWORD cchCustomActionData = 0;
286
287 //
288 // initialize
289 //
290 hr = WcaInitialize(hInstall, "WixCloseApplications");
291 ExitOnFailure(hr, "failed to initialize");
292
293 //
294 // loop through all the objects to be secured
295 //
296 hr = WcaOpenExecuteView(wzQUERY_CLOSEAPPS, &hView);
297 ExitOnFailure(hr, "failed to open view on WixCloseApplication table");
298 while (S_OK == (hr = WcaFetchRecord(hView, &hRec)))
299 {
300 hr = WcaGetRecordString(hRec, QCA_ID, &pwzId);
301 ExitOnFailure(hr, "failed to get id from WixCloseApplication table");
302
303 hr = WcaGetRecordString(hRec, QCA_CONDITION, &pwzCondition);
304 ExitOnFailure(hr, "failed to get condition from WixCloseApplication table");
305
306 if (pwzCondition && *pwzCondition)
307 {
308 condition = ::MsiEvaluateConditionW(hInstall, pwzCondition);
309 if (MSICONDITION_ERROR == condition)
310 {
311 hr = E_INVALIDARG;
312 ExitOnFailure(hr, "failed to process condition for WixCloseApplication '%ls'", pwzId);
313 }
314 else if (MSICONDITION_FALSE == condition)
315 {
316 continue; // skip processing this target
317 }
318 }
319
320 hr = WcaGetRecordFormattedString(hRec, QCA_TARGET, &pwzTarget);
321 ExitOnFailure(hr, "failed to get target from WixCloseApplication table");
322
323 hr = WcaGetRecordFormattedString(hRec, QCA_DESCRIPTION, &pwzDescription);
324 ExitOnFailure(hr, "failed to get description from WixCloseApplication table");
325
326 hr = WcaGetRecordInteger(hRec, QCA_ATTRIBUTES, reinterpret_cast<int*>(&dwAttributes));
327 ExitOnFailure(hr, "failed to get attributes from WixCloseApplication table");
328
329 hr = WcaGetRecordFormattedString(hRec, QCA_PROPERTY, &pwzProperty);
330 ExitOnFailure(hr, "failed to get property from WixCloseApplication table");
331
332 hr = WcaGetRecordInteger(hRec, QCA_TERMINATEEXITCODE, reinterpret_cast<int*>(&dwTerminateExitCode));
333 if (S_FALSE == hr)
334 {
335 dwTerminateExitCode = 0;
336 hr = S_OK;
337 }
338 ExitOnFailure(hr, "failed to get timeout from WixCloseApplication table");
339
340 hr = WcaGetRecordInteger(hRec, QCA_TIMEOUT, reinterpret_cast<int*>(&dwTimeout));
341 if (S_FALSE == hr)
342 {
343 dwTimeout = DEFAULT_PROCESS_EXIT_WAIT_TIME;
344 hr = S_OK;
345 }
346 ExitOnFailure(hr, "failed to get timeout from WixCloseApplication table");
347
348 // Before trying any changes to the machine, prompt if requested.
349 if (dwAttributes & CLOSEAPP_ATTRIBUTE_PROMPTTOCONTINUE)
350 {
351 hr = PromptToContinue(pwzTarget, pwzDescription ? pwzDescription : L"");
352 if (HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) == hr)
353 {
354 // Skip error message if user canceled.
355 ExitFunction();
356 }
357 ExitOnFailure(hr, "Failure while prompting user to continue to close application.");
358 }
359
360 //
361 // send WM_CLOSE or WM_QUERYENDSESSION to currently running applications
362 //
363 if (dwAttributes & CLOSEAPP_ATTRIBUTE_CLOSEMESSAGE)
364 {
365 SendApplicationMessage(pwzTarget, WM_CLOSE, dwTimeout);
366 }
367
368 if (dwAttributes & CLOSEAPP_ATTRIBUTE_ENDSESSIONMESSAGE)
369 {
370 SendApplicationMessage(pwzTarget, WM_QUERYENDSESSION, dwTimeout);
371 }
372
373 //
374 // Pass the targets to the deferred action in case the app comes back
375 // even if we close it now.
376 //
377 if (dwAttributes & (CLOSEAPP_ATTRIBUTE_ELEVATEDCLOSEMESSAGE | CLOSEAPP_ATTRIBUTE_ELEVATEDENDSESSIONMESSAGE | CLOSEAPP_ATTRIBUTE_REBOOTPROMPT | CLOSEAPP_ATTRIBUTE_TERMINATEPROCESS))
378 {
379 hr = WcaWriteStringToCaData(pwzTarget, &pwzCustomActionData);
380 ExitOnFailure(hr, "failed to add target data to CustomActionData");
381
382 hr = WcaWriteIntegerToCaData(dwAttributes, &pwzCustomActionData);
383 ExitOnFailure(hr, "failed to add attribute data to CustomActionData");
384
385 hr = WcaWriteIntegerToCaData(dwTimeout, &pwzCustomActionData);
386 ExitOnFailure(hr, "failed to add timeout data to CustomActionData");
387
388 hr = WcaWriteIntegerToCaData(dwTerminateExitCode, &pwzCustomActionData);
389 ExitOnFailure(hr, "failed to add timeout data to CustomActionData");
390 }
391
392 if (pwzProperty && *pwzProperty)
393 {
394 SetRunningProcessProperty(pwzTarget, pwzProperty);
395 }
396
397 ++cCloseApps;
398 }
399
400 // if we looped through all records all is well
401 if (E_NOMOREITEMS == hr)
402 {
403 hr = S_OK;
404 }
405 ExitOnFailure(hr, "failed while looping through all apps to close");
406
407 //
408 // Do the UI dance now.
409 //
410 /*
411
412 TODO: Do this eventually
413
414 if (cCloseApps)
415 {
416 while (TRUE)
417 {
418 for (DWORD i = 0; i < cCloseApps; ++i)
419 {
420 hr = WcaAddTempRecord(&hListboxTable, &hListboxColumns, L"ListBox", NULL, 0, 4, L"FileInUseProcess", i, target, description);
421 if (FAILED(hr))
422 {
423 }
424 }
425 }
426 }
427 */
428
429 //
430 // schedule the custom action and add to progress bar
431 //
432 if (pwzCustomActionData && *pwzCustomActionData)
433 {
434 Assert(0 < cCloseApps);
435
436 hr = WcaDoDeferredAction(PLATFORM_DECORATION(L"WixCloseApplicationsDeferred"), pwzCustomActionData, cCloseApps * COST_CLOSEAPP);
437 ExitOnFailure(hr, "failed to schedule WixCloseApplicationsDeferred action");
438 }
439
440LExit:
441 if (hListboxColumns)
442 {
443 ::MsiCloseHandle(hListboxColumns);
444 }
445 if (hListboxTable)
446 {
447 ::MsiCloseHandle(hListboxTable);
448 }
449
450 ReleaseStr(pwzCustomActionData);
451 ReleaseStr(pwzData);
452 ReleaseStr(pwzProperty);
453 ReleaseStr(pwzCondition);
454 ReleaseStr(pwzDescription);
455 ReleaseStr(pwzTarget);
456 ReleaseStr(pwzId);
457
458 if (FAILED(hr))
459 {
460 er = HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT) == hr ? ERROR_INSTALL_USEREXIT : ERROR_INSTALL_FAILURE;
461 }
462 return WcaFinalize(er);
463}
464
465
466/******************************************************************
467 WixCloseApplicationsDeferred - entry point for
468 WixCloseApplicationsDeferred Custom Action
469 called as Type 1025 CustomAction
470 (deferred binary DLL)
471
472 NOTE: deferred CustomAction since it modifies the machine
473 NOTE: CustomActionData == wzTarget\tdwAttributes\tdwTimeout\tdwTerminateExitCode\t...
474******************************************************************/
475extern "C" UINT __stdcall WixCloseApplicationsDeferred(
476 __in MSIHANDLE hInstall
477 )
478{
479 //AssertSz(FALSE, "debug WixCloseApplicationsDeferred");
480 HRESULT hr = S_OK;
481 DWORD er = ERROR_SUCCESS;
482
483 LPWSTR pwz = NULL;
484 LPWSTR pwzData = NULL;
485 LPWSTR pwzTarget = NULL;
486 DWORD dwAttributes = 0;
487 DWORD dwTimeout = 0;
488 DWORD dwTerminateExitCode = 0;
489
490 DWORD *prgProcessIds = NULL;
491 DWORD cProcessIds = 0;
492
493 //
494 // initialize
495 //
496 hr = WcaInitialize(hInstall, "WixCloseApplicationsDeferred");
497 ExitOnFailure(hr, "failed to initialize");
498
499 hr = WcaGetProperty(L"CustomActionData", &pwzData);
500 ExitOnFailure(hr, "failed to get CustomActionData");
501
502 WcaLog(LOGMSG_TRACEONLY, "CustomActionData: %ls", pwzData);
503
504 pwz = pwzData;
505
506 //
507 // loop through all the passed in data
508 //
509 while (pwz && *pwz)
510 {
511 hr = WcaReadStringFromCaData(&pwz, &pwzTarget);
512 ExitOnFailure(hr, "failed to process target from CustomActionData");
513
514 hr = WcaReadIntegerFromCaData(&pwz, reinterpret_cast<int*>(&dwAttributes));
515 ExitOnFailure(hr, "failed to process attributes from CustomActionData");
516
517 hr = WcaReadIntegerFromCaData(&pwz, reinterpret_cast<int*>(&dwTimeout));
518 ExitOnFailure(hr, "failed to process timeout from CustomActionData");
519
520 hr = WcaReadIntegerFromCaData(&pwz, reinterpret_cast<int*>(&dwTerminateExitCode));
521 ExitOnFailure(hr, "failed to process terminate exit code from CustomActionData");
522
523 WcaLog(LOGMSG_VERBOSE, "Checking for App: %ls Attributes: %d", pwzTarget, dwAttributes);
524
525 //
526 // send WM_CLOSE or WM_QUERYENDSESSION to currently running applications
527 //
528 if (dwAttributes & CLOSEAPP_ATTRIBUTE_ELEVATEDCLOSEMESSAGE)
529 {
530 SendApplicationMessage(pwzTarget, WM_CLOSE, dwTimeout);
531 }
532
533 if (dwAttributes & CLOSEAPP_ATTRIBUTE_ELEVATEDENDSESSIONMESSAGE)
534 {
535 SendApplicationMessage(pwzTarget, WM_QUERYENDSESSION, dwTimeout);
536 }
537
538 // If we find that an app that we need closed is still runing, require a
539 // restart or kill the process as directed.
540 ProcFindAllIdsFromExeName(pwzTarget, &prgProcessIds, &cProcessIds);
541 if (0 < cProcessIds)
542 {
543 if (dwAttributes & CLOSEAPP_ATTRIBUTE_REBOOTPROMPT)
544 {
545 WcaLog(LOGMSG_VERBOSE, "App: %ls found running, requiring a reboot.", pwzTarget);
546
547 WcaDeferredActionRequiresReboot();
548 }
549 else if (dwAttributes & CLOSEAPP_ATTRIBUTE_TERMINATEPROCESS)
550 {
551 TerminateProcesses(prgProcessIds, cProcessIds, dwTerminateExitCode);
552 }
553 }
554
555 hr = WcaProgressMessage(COST_CLOSEAPP, FALSE);
556 ExitOnFailure(hr, "failed to send progress message");
557 }
558
559LExit:
560 ReleaseMem(prgProcessIds);
561
562 ReleaseStr(pwzTarget);
563 ReleaseStr(pwzData);
564
565 if (FAILED(hr))
566 {
567 er = ERROR_INSTALL_FAILURE;
568 }
569 return WcaFinalize(er);
570}