mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Installer] End a launcher that has no window to ask instead of prompting (#7681)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Installer] End a launcher that has no window to ask instead of prompting The in-app updater starts this installer detached and then closes itself. A client that accepts that close hides its window before it exits, and if it then hangs in shutdown there is nothing left to ask: the holder probe deliberately never posts WM_CLOSE to an instance it cannot see a window for, so the "ask it to close again" answer only repeated itself until the user picked Cancel and the installation aborted (Sentry COCKATRICE-DESKTOP-J). The launcher wait now asks the launcher itself before asking the user. A new probe mode, --close-launcher, answers whether that one process has a window a close could reach at all; it is limited to a pid that still runs from the directory being replaced, so a reused process id cannot be hit. When there is no window, the installer gives the shutdown it already started one full grace and then ends it through the handle it waited on, which is what the dialog's Yes answer did. The prompt remains for the case it exists for: a window is up, the instance may be waiting on its own question, and No now means only "keep waiting". The client is left alone: an already-installed version runs the installer during its own update, so a change there cannot reach the run that aborts. * [libcockatrice_rng] Include immintrin.h for AVX2 builds in SFMT --------- Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
This commit is contained in:
parent
5cf3358c99
commit
06da7f815a
5 changed files with 233 additions and 41 deletions
|
|
@ -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)'
|
||||
|
|
|
|||
|
|
@ -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 <immintrin.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* SFMT internal state
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<HWND> &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<HANDLE> &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<Holder> &holders, unsigned long waitMilliseconds)
|
||||
{
|
||||
CloseHoldersResult result;
|
||||
|
|
@ -233,18 +234,20 @@ CloseHoldersResult closeCockatriceHolders(const std::vector<Holder> &holders, un
|
|||
search.processId = holder.processId;
|
||||
EnumWindows(collectProcessWindow, reinterpret_cast<LPARAM>(&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<Holder> &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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "restart_manager_holders.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
|
@ -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,11 +56,14 @@ 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
|
||||
|
|
@ -60,6 +71,7 @@ enum class Mode
|
|||
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<DWORD>(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<wchar_t> 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue