diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index cda370670..bc121ff25 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -74,6 +74,11 @@ Var WaitSlice ; be ended from the answer the dialog offers. Cleared when only the synchronize right could ; be had (see WaitForLauncherToExit). Var WaitCanEnd +; How far the launcher wait got with a launcher that has no window to ask: 0 before the +; first round, 1 after the grace it was given, 2 after it was ended. A launcher whose +; window is gone has answered its own close and is stuck in shutdown, so the dialog cannot +; reach it and the wait may end it instead of asking the user (see WaitForLauncherToExit). +Var WaitWindowless Var InstallLogPath Var LogHandle Var LogMessage @@ -1241,6 +1246,33 @@ Function AbortInstall Abort FunctionEnd +; usage: set $GuardDir and $WaitPid, then call this. Asks the one process the installer +; waits on to close and sets $R3 to what it could do: 1 = a window exists and was asked to +; close (or is waiting on a question of its own), 4 = it has no window a user could answer, +; anything else = the probe could not answer. Only 4 lets the wait act on its own; every +; other code keeps the user in the decision, because the installer holds a live handle and +; "the probe did not find it" is not the same answer as "it is gone". +; +; The guard's CloseHolders answers the same question for every holder and leaves a +; windowless instance to the user, which is right there: the prompt names it. The launcher +; is the one process nobody can be asked about - the in-app updater runs the installer +; silent and detached, and a client that has already hidden its window has answered its +; own prompts - so this answer has to say whether there was a window to ask at all. +Function CloseLauncher + Call ExtractHolderProbe + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")' + System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_LAUNCHER_PID", "$WaitPid")' + ; The probe waits 30 s itself when it posts a close. The timeout here only bounds a + ; probe that never returns; the Restart Manager call the other modes make is not + ; reached in this one. + nsExec::ExecToStack /TIMEOUT=40000 '"$PLUGINSDIR\installer-holder-probe.exe" --close-launcher' + Pop $R3 + Pop $R4 + Push "close-launcher: probe exit=$R3 [$R4]" + Call CockLogWrite + DetailPrint "Closing Cockatrice: $R4" +FunctionEnd + ; Waits for the application that launched this installer to exit, using the /PID= ; it passed. Sets $LauncherExited to 1 when that process is provably gone. ; @@ -1255,8 +1287,16 @@ FunctionEnd ; Sets $LauncherExited to 0 when no usable /PID= was given, which is the older ; client that does not pass one; the caller then has no authority available and ; falls back to the holder probe. +; +; A launcher that survives the first grace is asked to close by the probe, and the +; probe also answers whether there was a window a close could reach. A client hides +; its window the moment its own close is accepted, so a launcher without one has +; already answered its own prompts and is stuck in shutdown: no dialog can reach it +; there, and the wait ends it after one more grace instead of asking the user a +; question whose "keep waiting" answer can never change anything. Function WaitForLauncherToExit StrCpy $LauncherExited 0 + StrCpy $WaitWindowless 0 !insertmacro CockLog "wait-launcher: pid='$WaitPid'" ${If} $WaitPid == "" !insertmacro CockLog "wait-launcher: no pid given, falling back to name check" @@ -1324,14 +1364,58 @@ Function WaitForLauncherToExit ; below has to act on that process and not on whatever $WaitPid names by then. !insertmacro CockLog "wait-launcher: did not exit within $WaitSlice ms (wait=$R1, canEnd=$WaitCanEnd)" DetailPrint "The application that started this installer did not exit in time." - ; Same three answers as every other prompt that waits on a running program, and - ; "carry on anyway" is not one of them: a live process has its image and its Qt - ; DLLs mapped, so writing the payload over them is the broken install this wait - ; exists to prevent. Yes ends the launcher, No asks it to close again and keeps - ; waiting, Cancel cancels - the launcher is the client the user started this update - ; from, and ending it is the same offer the guard makes a moment later. + ; Ask the launcher itself before asking the user. A client that has already hidden + ; its window has answered its own prompts and is stuck in shutdown: there is nothing + ; left a dialog could reach, and the "ask it to close again" answer the dialog used + ; to offer could only repeat itself until the user cancelled. + StrCpy $GuardDir $INSTDIR + Call CloseLauncher + ; $R3 = what the probe could do (see CloseLauncher), $R4 = its line, already logged. + ; A process that answered the close during the probe's own wait is gone by now. + System::Call 'kernel32::WaitForSingleObject(p R0, i 0) i.R1' + ${If} $R1 == 0 + System::Call 'kernel32::CloseHandle(p R0)' + StrCpy $R0 0 + Goto wait_launcher_open + ${EndIf} + ${If} $R3 == "4" + ; Nothing a user could answer: the window is gone, so the launcher is past its + ; own prompts and stuck in shutdown. It cannot be asked, and it is not going to + ; ask the user anything either, so the dialog below would be unanswerable. + ${If} $WaitCanEnd == 1 + ${If} $WaitWindowless == 0 + ; Hiding the window is what the client does when its close is accepted, + ; so what remains is the shutdown it already started: one full grace for + ; that, without a question nobody could act on. + StrCpy $WaitWindowless 1 + StrCpy $WaitSlice 60000 + !insertmacro CockLog "wait-launcher: no window to ask, giving it one more minute" + System::Call 'kernel32::CloseHandle(p R0)' + StrCpy $R0 0 + Goto wait_launcher_open + ${EndIf} + ${If} $WaitWindowless == 1 + ; It did not use that minute either. Ending it is what the dialog's Yes + ; answer would have done, and there is no question left to put to the + ; user: the instance lost its window before this round started. + StrCpy $WaitWindowless 2 + !insertmacro CockLog "wait-launcher: still no window to ask; ending the launcher" + DetailPrint "The application that started this installer could not be asked to close; ending it." + System::Call 'kernel32::TerminateProcess(p R0, i 1) i.R2' + !insertmacro CockLog "wait-launcher: ending the launcher returned $R2" + System::Call 'kernel32::CloseHandle(p R0)' + StrCpy $R0 0 + Goto wait_launcher_open + ${EndIf} + ${EndIf} + ${EndIf} + ; A window exists, or the launcher cannot be ended from here: the three answers + ; below. "Carry on anyway" is not one of them - a live process has its image and its + ; Qt DLLs mapped, so writing the payload over them is the broken install this wait + ; exists to prevent. Yes ends the launcher, Cancel cancels, and No keeps waiting: + ; the close and the bring-forward just happened above. MessageBox MB_YESNOCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON2 \ - "Cockatrice has not finished closing, so the installation is paused rather than replace files it is still using.$\r$\n$\r$\nIt may be waiting for you to answer its own question about an unsaved deck, an open game or a card database update.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or an unwritten setting is lost.$\r$\n$\r$\nChoose No to bring it to the front, ask it to close again and keep waiting, or Cancel to cancel the installation." \ + "Cockatrice has not finished closing, so the installation is paused rather than replace files it is still using.$\r$\n$\r$\nIt may be waiting for you to answer its own question about an unsaved deck, an open game or a card database update.$\r$\n$\r$\nChoose Yes to end it now - an unsaved deck, a running game or an unwritten setting is lost.$\r$\n$\r$\nChoose No to keep waiting, or Cancel to cancel the installation." \ IDYES wait_launcher_end IDNO wait_launcher_retry !insertmacro CockLog "wait-launcher: cancelled by user" StrCpy $AbortReason \ @@ -1357,14 +1441,12 @@ Function WaitForLauncherToExit StrCpy $R0 0 ; The question has been asked and answered once; from here the wait is the full ; grace again, so a slow flush or a confirmation being answered does not turn - ; into a stream of prompts. + ; into a stream of prompts. The close itself and the bring-forward already + ; happened just before the dialog, so this only waits. The windowless grace starts + ; over as well: "keep waiting" must not be answered by ending the process a minute + ; later without asking again. + StrCpy $WaitWindowless 0 StrCpy $WaitSlice 60000 - ; The same request the window's close button makes, and the same close the guard - ; would send a moment later. It brings the client forward as well, which is the - ; part that helps when the confirmation is behind something else. Nothing is - ; force-stopped here: the answer that does that is Yes. - StrCpy $GuardDir $INSTDIR - Call CloseHolders Goto wait_launcher_open ${EndIf} System::Call 'kernel32::CloseHandle(p R0)' diff --git a/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h index 34d9e746f..df114cf9b 100644 --- a/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h +++ b/libcockatrice_rng/libcockatrice/rng/sfmt/SFMT.h @@ -113,6 +113,14 @@ union W128_T { /** 128-bit data type */ typedef union W128_T w128_t; +#if defined(__AVX2__) +/* The AVX2 state below names __m256i (and __m512i under AVX-512VL). The SIMD header + above is only pulled in for HAVE_SSE2, so a build that has __AVX2__ from the compiler + (e.g. -march=native) but not HAVE_SSE2 from the build system would otherwise not have + those types declared. */ + #include +#endif + /** * SFMT internal state */ diff --git a/tools/installer-holder-probe/close_holders.cpp b/tools/installer-holder-probe/close_holders.cpp index 791347408..d1831e4e9 100644 --- a/tools/installer-holder-probe/close_holders.cpp +++ b/tools/installer-holder-probe/close_holders.cpp @@ -15,8 +15,6 @@ namespace { -constexpr wchar_t COCKATRICE_IMAGE[] = L"cockatrice.exe"; - /// How long one poll of the asked processes takes. One WaitForSingleObject per process /// and no enumeration, so polling costs nothing next to the seconds this is called on. constexpr unsigned long WAIT_POLL_MILLISECONDS = 200; @@ -26,11 +24,6 @@ constexpr unsigned long WAIT_POLL_MILLISECONDS = 200; /// the line stays far below it. constexpr std::size_t REPORT_CHAR_MAX = 900; -bool isCockatriceImage(const std::wstring &imageName) -{ - return CompareStringOrdinal(imageName.c_str(), -1, COCKATRICE_IMAGE, -1, TRUE) == CSTR_EQUAL; -} - /// The holder as the log has to name it to be actionable. Deliberately not /// formatHolder(): that one is worded for the dialog, where ", running" says something /// the line here continues after. @@ -112,7 +105,7 @@ struct CloseTarget { HWND main = nullptr; ///< first visible window without an owner: the application's own HWND toRaise = nullptr; ///< topmost visible window: a dialog when one is up - bool modalOpen = false; ///< the window a close would reach is disabled, so a dialog of its own is up + bool modalOpen = false; ///< the only window a user could answer is the instance's own question }; /// Only the windows the user could answer are candidates - see isAnswerableWindow. An @@ -136,9 +129,11 @@ CloseTarget chooseCloseTarget(const std::vector &windows) } target.toRaise = topmost != nullptr ? topmost : target.main; // A modal dialog disables the windows behind it, so a disabled main window is the - // instance asking a question of its own. - target.modalOpen = - target.toRaise != nullptr && IsWindowEnabled(target.main != nullptr ? target.main : target.toRaise) == FALSE; + // instance asking a question of its own. A visible window with no unowned main window + // next to it is one too: what is on screen is then the question itself - the window + // behind it is hidden, not gone - and there is no window a close could be posted to + // without hiding the question with it. + target.modalOpen = target.toRaise != nullptr && (target.main == nullptr || IsWindowEnabled(target.main) == FALSE); return target; } @@ -213,6 +208,12 @@ bool waitForExit(const std::vector &processes, unsigned long waitMillise } } // namespace +bool isCockatriceImage(const std::wstring &imageName) +{ + constexpr wchar_t COCKATRICE_IMAGE[] = L"cockatrice.exe"; + return CompareStringOrdinal(imageName.c_str(), -1, COCKATRICE_IMAGE, -1, TRUE) == CSTR_EQUAL; +} + CloseHoldersResult closeCockatriceHolders(const std::vector &holders, unsigned long waitMilliseconds) { CloseHoldersResult result; @@ -233,18 +234,20 @@ CloseHoldersResult closeCockatriceHolders(const std::vector &holders, un search.processId = holder.processId; EnumWindows(collectProcessWindow, reinterpret_cast(&search)); if (search.windows.empty()) { + ++result.withoutWindow; lines.push_back(processLabel(holder) + L" has no window to close, left to you"); continue; } const CloseTarget target = chooseCloseTarget(search.windows); - if (target.main == nullptr) { + if (target.toRaise == nullptr) { // Nothing visible that a user could answer: the instance is hidden - in the // notification area, say - or has nothing but Qt's own bookkeeping windows on // screen. Neither is a window a close could reach, and showing one from here does // not help: Windows would display it while the application still believes it is // hidden, so it stays blank and answers nothing. The instance is named and left to // the user, who can open it from the notification area. + ++result.withoutWindow; lines.push_back(processLabel(holder) + L" has no window to ask, left to you"); continue; } @@ -255,7 +258,8 @@ CloseHoldersResult closeCockatriceHolders(const std::vector &holders, un // an earlier attempt useless: the client accepts it, hides this window and the dialog // with it, and stays alive inside the nested loop the dialog opened, where nothing can // reach it any more. The question is the request now, so it is brought forward and - // left alone. + // left alone - including a question whose owner window is hidden, which is the only + // thing left on screen and has no unowned window a close could be posted to. if (target.modalOpen) { lines.push_back(line + L", and is waiting for an answer to a question of its own"); continue; diff --git a/tools/installer-holder-probe/close_holders.h b/tools/installer-holder-probe/close_holders.h index 0e2b93b26..643624779 100644 --- a/tools/installer-holder-probe/close_holders.h +++ b/tools/installer-holder-probe/close_holders.h @@ -11,13 +11,27 @@ * * @c report is one line naming every holder, including the ones this deliberately * leaves alone, because the installer repeats it in its log and in the second prompt. + * @c withoutWindow counts the holders that had no visible window a user could answer: they + * were named and left alone, and the launcher mode reads that count as "nothing to ask" + * rather than as a window the user still has to answer. */ struct CloseHoldersResult { - unsigned int askedToClose = 0; ///< holders a WM_CLOSE was posted to - std::wstring report; ///< one line, narrowed to ASCII when written + unsigned int askedToClose = 0; ///< holders a WM_CLOSE was posted to + unsigned int withoutWindow = 0; ///< holders with no visible window a user could answer + std::wstring report; ///< one line, narrowed to ASCII when written }; +/** + * @brief Whether @p imageName is the client the update belongs to, matched without case. + * + * The close mode asks only this image to close, and the launcher mode is only allowed to + * report it as safe to end, so both read the answer from here. + * + * @param imageName the file name of a process image, empty when it could not be read + */ +bool isCockatriceImage(const std::wstring &imageName); + /** * @brief Brings every running Cockatrice holding an installer file forward and asks it * to close. diff --git a/tools/installer-holder-probe/main.cpp b/tools/installer-holder-probe/main.cpp index 0cbecb9f2..10a6c22e4 100644 --- a/tools/installer-holder-probe/main.cpp +++ b/tools/installer-holder-probe/main.cpp @@ -3,6 +3,7 @@ #include "restart_manager_holders.h" #include +#include #include #include #include @@ -14,6 +15,10 @@ namespace /// trailing backslash then never has to survive NSIS quoting or a command line. constexpr wchar_t INSTALL_DIR_VARIABLE[] = L"COCKATRICE_GUARD_DIR"; +/// The installer sets this to the process it waits on (/PID=), so the launcher mode reads +/// the same process id the wait does instead of one that has to survive NSIS quoting. +constexpr wchar_t LAUNCHER_PID_VARIABLE[] = L"COCKATRICE_LAUNCHER_PID"; + /// How long the close mode waits for the instance it asked to exit before it reports the /// process as still running. The installer measures the purge right after this call, so /// the wait belongs here: it is the only place that holds a handle on the process whose @@ -30,16 +35,19 @@ constexpr unsigned long END_WAIT_MILLISECONDS = 5000; /// In the close mode 1 means a close was actually sent and 0 that every holder was left /// to the user, which is what the report line next to the code spells out. 3 is only for /// a lookup that failed and named nobody: one that failed but still named a process -/// running from the directory is answered with that process. +/// running from the directory is answered with that process. 4 is only the launcher mode: +/// the process the installer waits on has no window a user could answer, which is the one +/// answer the installer acts on by itself instead of asking the user. enum ExitCode { EXIT_NO_HOLDERS = 0, EXIT_HOLDERS_FOUND = 1, EXIT_NOT_CONFIGURED = 2, EXIT_PROBE_FAILED = 3, + EXIT_NO_WINDOW = 4, }; -/// The probe has three modes, selected by the first argument: +/// The probe has four modes, selected by the first argument: /// /// (none) name what holds the payload, one line, for the guard prompt and the /// abort dialog/log @@ -48,18 +56,22 @@ enum ExitCode /// --end-holders end the running Cockatrice, Oracle and Servatrice and wait for them to /// be gone, for the last resort the guard prompt offers when a close did /// not free the files +/// --close-launcher ask the one process the installer waits on (/PID=) to close and say +/// whether there was a window to ask at all, because an instance with +/// no window has answered its own close and is stuck in shutdown /// -/// All three answer from two lookups that cover each other's blind spot: the Restart Manager -/// names whoever has a payload file open, and every process running from the directory is -/// named as well, because a running instance holds its own image and the modules it -/// loaded as mappings rather than open files and the Restart Manager does not always +/// The guard's three modes answer from two lookups that cover each other's blind spot: the +/// Restart Manager names whoever has a payload file open, and every process running from the +/// directory is named as well, because a running instance holds its own image and the modules +/// it loaded as mappings rather than open files and the Restart Manager does not always /// report those. Neither lookup alone is the guard's authority: an instance the file /// handles do not name is exactly the case that left a half-replaced install behind. enum class Mode { - Name, ///< name the holders - Close, ///< ask the holders to close - End, ///< end the holders + Name, ///< name the holders + Close, ///< ask the holders to close + End, ///< end the holders + CloseLauncher, ///< ask the one process the installer waits on to close }; Mode requestedMode(int argumentCount, char **arguments) @@ -72,6 +84,9 @@ Mode requestedMode(int argumentCount, char **arguments) if (mode == "--end-holders") { return Mode::End; } + if (mode == "--close-launcher") { + return Mode::CloseLauncher; + } } return Mode::Name; } @@ -92,11 +107,11 @@ void writeLine(const std::wstring &text) WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), narrow.data(), static_cast(narrow.size()), &written, nullptr); } -std::wstring installDirFromEnvironment() +std::wstring environmentVariable(const wchar_t *name) { for (DWORD size = MAX_PATH; size <= 32768U; size *= 2) { std::vector buffer(size); - const DWORD length = GetEnvironmentVariableW(INSTALL_DIR_VARIABLE, buffer.data(), size); + const DWORD length = GetEnvironmentVariableW(name, buffer.data(), size); if (length == 0) { return {}; } @@ -106,6 +121,68 @@ std::wstring installDirFromEnvironment() } return {}; } + +std::wstring installDirFromEnvironment() +{ + return environmentVariable(INSTALL_DIR_VARIABLE); +} + +/// The process id the installer waits on, or false when it was not passed or is not a +/// number. A pid of 0 is refused because the installer never passes one. +bool launcherProcessIdFromEnvironment(unsigned long &processId) +{ + const std::wstring text = environmentVariable(LAUNCHER_PID_VARIABLE); + if (text.empty()) { + return false; + } + wchar_t *end = nullptr; + const unsigned long parsed = std::wcstoul(text.c_str(), &end, 10); + if (parsed == 0 || end == nullptr || *end != L'\0') { + return false; + } + processId = parsed; + return true; +} + +/// Asks the one process the installer waits on to close and answers what it could do. +/// +/// The guard closes a set and reports every holder; here there is exactly one process, and +/// the installer needs an answer it can act on rather than a sentence: a launcher with no +/// window to ask is past its own prompts and stuck in shutdown, so no answer the installer +/// offers would ever reach it. Everything else - a window that was asked, or one that is +/// waiting on a question of its own - is still the user's decision. +int closeLauncher(const std::wstring &installDir) +{ + unsigned long processId = 0; + if (!launcherProcessIdFromEnvironment(processId)) { + writeLine(L"the probe was not given the launcher process id"); + return EXIT_NOT_CONFIGURED; + } + + // Only an instance that still runs from the directory being replaced is asked: a bare + // process id names anything once it is reused, while a process running from the + // directory is the one the installer started this update from. + for (const Holder &holder : findProcessesRunningFrom(installDir)) { + if (holder.processId != processId) { + continue; + } + if (!isCockatriceImage(holder.imageName)) { + writeLine(L"the launcher is not cockatrice.exe, left to you"); + return EXIT_PROBE_FAILED; + } + const CloseHoldersResult result = closeCockatriceHolders({holder}, CLOSE_WAIT_MILLISECONDS); + writeLine(result.report); + if (result.askedToClose > 0) { + return EXIT_HOLDERS_FOUND; + } + return result.withoutWindow > 0 ? EXIT_NO_WINDOW : EXIT_HOLDERS_FOUND; + } + // Not an answer: the lookup may have failed or refused the image, and the installer + // holds a live handle on that process, so "not found" must not read as "gone". The + // installer shows its dialog for this code instead of acting on it. + writeLine(L"the launcher is not running from the directory being replaced, left to you"); + return EXIT_PROBE_FAILED; +} } // namespace int main(int argumentCount, char **arguments) @@ -116,6 +193,14 @@ int main(int argumentCount, char **arguments) return EXIT_NOT_CONFIGURED; } + const Mode mode = requestedMode(argumentCount, arguments); + // The launcher mode answers about exactly one process, so the holder set below is what it + // does not need - and the Restart Manager call behind it is the slowest thing this tool + // does, which must not delay a wait that started with a ten-second grace. + if (mode == Mode::CloseLauncher) { + return closeLauncher(installDir); + } + // Processes running from the directory first: they are the instance the user is asked // to close, and the Restart Manager may not name any of them (see // findProcessesRunningFrom). The file holders follow, deduplicated by process, so an @@ -147,7 +232,6 @@ int main(int argumentCount, char **arguments) return EXIT_PROBE_FAILED; } - const Mode mode = requestedMode(argumentCount, arguments); if (holders.empty()) { // No output in the naming mode: the installer words the "nothing is holding them" // case, and it owns the message either way. The acting modes say it themselves,