mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-07 18:43:26 -08:00
Merge branch 'master' into tooomm-doxy_clean
This commit is contained in:
commit
32f027e880
14 changed files with 754 additions and 29 deletions
2
.github/workflows/documentation-build.yml
vendored
2
.github/workflows/documentation-build.yml
vendored
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
- name: "Install Doxygen"
|
||||
uses: ssciwr/doxygen-install@v2
|
||||
with:
|
||||
version: "1.16.1"
|
||||
version: "1.18.0"
|
||||
|
||||
- name: "Update Doxygen Configuration"
|
||||
run: |
|
||||
|
|
|
|||
97
Doxyfile
97
Doxyfile
|
|
@ -1,4 +1,4 @@
|
|||
# Doxyfile 1.16.1
|
||||
# Doxyfile 1.18.0
|
||||
|
||||
# This file describes the settings to be used by the documentation system
|
||||
# Doxygen (www.doxygen.org) for a project.
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
# TAG = value [value, ...]
|
||||
# For lists, items can also be appended using:
|
||||
# TAG += value [value, ...]
|
||||
# Values that contain spaces should be placed between quotes (\" \").
|
||||
# Values that contain spaces should be placed between quotes (" ").
|
||||
#
|
||||
# Note:
|
||||
#
|
||||
|
|
@ -81,7 +81,7 @@ OUTPUT_DIRECTORY = docs
|
|||
# and will distribute the generated files over these directories. Enabling this
|
||||
# option can be useful when feeding Doxygen a huge amount of source files, where
|
||||
# putting all generated files in the same directory would otherwise cause
|
||||
# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to
|
||||
# performance problems for the file system. Adjust CREATE_SUBDIRS_LEVEL to
|
||||
# control the number of sub-directories.
|
||||
# The default value is: NO.
|
||||
|
||||
|
|
@ -802,7 +802,7 @@ GENERATE_REQUIREMENTS = YES
|
|||
|
||||
# The REQ_TRACEABILITY_INFO tag controls if traceability information is shown on
|
||||
# the requirements page (only relevant when using \requirement comment blocks).
|
||||
# The setting NO will disable the traceablility information altogether. The
|
||||
# The setting NO will disable the traceability information altogether. The
|
||||
# setting UNSATISFIED_ONLY will show a list of requirements that are missing a
|
||||
# satisfies relation (through the command: \satisfies). Similarly the setting
|
||||
# UNVERIFIED_ONLY will show a list of requirements that are missing a verifies
|
||||
|
|
@ -855,8 +855,8 @@ SHOW_NAMESPACES = YES
|
|||
# The FILE_VERSION_FILTER tag can be used to specify a program or script that
|
||||
# Doxygen should invoke to get the current version for each file (typically from
|
||||
# the version control system). Doxygen will invoke the program by executing (via
|
||||
# popen()) the command command input-file, where command is the value of the
|
||||
# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
|
||||
# popen()) the command command input_file, where command is the value of the
|
||||
# FILE_VERSION_FILTER tag, and input_file is the name of an input file provided
|
||||
# by Doxygen. Whatever the program writes to standard output is used as the file
|
||||
# version. For an example see the documentation.
|
||||
|
||||
|
|
@ -1643,8 +1643,8 @@ DOCSET_PUBLISHER_NAME = Publisher
|
|||
# a.o. the download links, offline (the HTML help workshop was already many
|
||||
# years in maintenance mode). You can download the HTML help workshop from the
|
||||
# web archives at Installation executable (see:
|
||||
# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo
|
||||
# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
|
||||
# http://web.archive.org/web/20160201063255/https://download.microsoft.com/downl
|
||||
# oad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
|
||||
#
|
||||
# The HTML Help Workshop contains a compiler that can convert all HTML output
|
||||
# generated by Doxygen into a single compiled HTML file (.chm). Compiled HTML
|
||||
|
|
@ -1951,7 +1951,7 @@ MATHJAX_FORMAT = HTML-CSS
|
|||
# output directory using the MATHJAX_RELPATH option. For Mathjax version 2 the
|
||||
# destination directory should contain the MathJax.js script. For instance, if
|
||||
# the mathjax directory is located at the same level as the HTML output
|
||||
# directory, then MATHJAX_RELPATH should be ../mathjax.s For Mathjax versions 3
|
||||
# directory, then MATHJAX_RELPATH should be ../mathjax. For Mathjax versions 3
|
||||
# and 4 the destination directory should contain the tex-<format>.js script
|
||||
# (where <format> is either chtml or svg). The default value points to the
|
||||
# MathJax Content Delivery Network so you can quickly see the result without
|
||||
|
|
@ -2172,9 +2172,8 @@ EXTRA_PACKAGES =
|
|||
#
|
||||
# Note: Only use a user-defined header if you know what you are doing!
|
||||
# Note: The header is subject to change so you typically have to regenerate the
|
||||
# default header when upgrading to a newer version of Doxygen. The following
|
||||
# commands have a special meaning inside the header (and footer): For a
|
||||
# description of the possible markers and block names see the documentation.
|
||||
# default header when upgrading to a newer version of Doxygen. For a description
|
||||
# of the possible markers and block names see the documentation.
|
||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||
|
||||
LATEX_HEADER =
|
||||
|
|
@ -2657,6 +2656,19 @@ HAVE_DOT = YES
|
|||
|
||||
DOT_NUM_THREADS = 0
|
||||
|
||||
# The DOT_BATCH_SIZE specifies the number of dot graphs Doxygen is allowed to
|
||||
# compile in a single invocation of dot. When set to 1 Doxygen will invoke dot
|
||||
# for each graph separately, which can cause significant process creation
|
||||
# overhead especially on systems with many CPU cores. Together with
|
||||
# DOT_NUM_THREADS this setting can be used to optimise the dot processing speed
|
||||
# for a particular system. Doxygen will try to give each thread a balanced batch
|
||||
# of work. If the total number of graphs to process exceeds DOT_NUM_THREADS *
|
||||
# DOT_BATCH_SIZE then additional batches will be created for dot to process.
|
||||
# Minimum value: 1, maximum value: 1000, default value: 50.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_BATCH_SIZE = 500
|
||||
|
||||
# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
|
||||
# subgraphs. When you want a differently looking font in the dot files that
|
||||
# Doxygen generates you can specify fontname, fontcolor and fontsize attributes.
|
||||
|
|
@ -2948,6 +2960,58 @@ PLANTUML_INCLUDE_PATH =
|
|||
|
||||
PLANTUMLFILE_DIRS =
|
||||
|
||||
# When using Mermaid diagrams with CLI rendering, the MERMAID_PATH tag should be
|
||||
# used to specify the directory where the mmdc (Mermaid CLI) executable can be
|
||||
# found. If left blank, CLI-based rendering is disabled. For HTML output,
|
||||
# client-side rendering via JavaScript is used by default and does not require
|
||||
# mmdc. For LaTeX/PDF output, mmdc is required to pre-generate images. Doxygen
|
||||
# will generate a warning when CLI rendering is needed but mmdc is not
|
||||
# available.
|
||||
|
||||
MERMAID_PATH =
|
||||
|
||||
# When using Mermaid diagrams, the MERMAID_CONFIG_FILE tag can be used to
|
||||
# specify a JSON configuration file for the Mermaid CLI tool (mmdc). This file
|
||||
# can contain theme settings and other Mermaid configuration options.
|
||||
|
||||
MERMAID_CONFIG_FILE =
|
||||
|
||||
# The MERMAID_RENDER_MODE tag selects how Mermaid diagrams are rendered.
|
||||
# Possible values are: AUTO (use client-side rendering for HTML and mmdc for
|
||||
# LaTeX/PDF and other formats. If MERMAID_PATH is not set, non-HTML diagrams
|
||||
# will produce a warning), CLI (use the mmdc tool to pre-generate images
|
||||
# (requires Node.js and mermaid-js/mermaid-cli). Works for all output formats)
|
||||
# and CLIENT_SIDE (embed mermaid.js in HTML output for client-side rendering.
|
||||
# Does not require mmdc but only works for HTML output).
|
||||
# The default value is: AUTO.
|
||||
|
||||
MERMAID_RENDER_MODE = AUTO
|
||||
|
||||
# The MERMAID_JS_URL tag specifies the URL to load mermaid.js from when using
|
||||
# client-side rendering (MERMAID_RENDER_MODE is CLIENT_SIDE or AUTO). The
|
||||
# default points to the latest Mermaid v11 release on the jsDelivr CDN.
|
||||
#
|
||||
# The default CDN URL requires internet access when viewing the generated
|
||||
# documentation. For offline use, download mermaid.esm.min.mjs and set this to a
|
||||
# relative path, or use MERMAID_RENDER_MODE=CLI to pre-generate images instead.
|
||||
# Examples:
|
||||
# - Latest v11 (default):
|
||||
# https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs
|
||||
# - Pinned version:
|
||||
# https://cdn.jsdelivr.net/npm/mermaid@11.3.0/dist/mermaid.esm.min.mjs
|
||||
# - Local copy: ./mermaid.esm.min.mjs (user must place file in HTML output
|
||||
# directory)
|
||||
# The default value is:
|
||||
# https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs.
|
||||
|
||||
MERMAID_JS_URL = https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs
|
||||
|
||||
# The MERMAIDFILE_DIRS tag can be used to specify one or more directories that
|
||||
# contain Mermaid files that are included in the documentation (see the
|
||||
# \mermaidfile command).
|
||||
|
||||
MERMAIDFILE_DIRS =
|
||||
|
||||
# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
|
||||
# that will be shown in the graph. If the number of nodes in a graph becomes
|
||||
# larger than this value, Doxygen will truncate the graph, which is visualized
|
||||
|
|
@ -2972,15 +3036,6 @@ DOT_GRAPH_MAX_NODES = 50
|
|||
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
|
||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
|
||||
# this, this feature is disabled by default.
|
||||
# The default value is: NO.
|
||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||
|
||||
DOT_MULTI_TARGETS = NO
|
||||
|
||||
# If the GENERATE_LEGEND tag is set to YES Doxygen will generate a legend page
|
||||
# explaining the meaning of the various boxes and arrows in the dot generated
|
||||
# graphs.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ Var NormalDestDir
|
|||
Var PortableDestDir
|
||||
Var PortableMode
|
||||
Var ReinstallMode
|
||||
Var AllowForceClose
|
||||
Var GuardDir
|
||||
Var OldUninstallString
|
||||
|
||||
!include LogicLib.nsh
|
||||
!include FileFunc.nsh
|
||||
|
|
@ -131,6 +134,20 @@ ${EndIf}
|
|||
; Now that $PortableMode reflects reality, commit InstDir into the correct slot
|
||||
Call SetModeDestinationFromInstdir
|
||||
|
||||
; Make sure no application instance is still running (and holding file locks)
|
||||
; before the previous version is uninstalled. On a silent update (/R /S) the
|
||||
; running processes are asked to close gracefully and waited for; Cockatrice
|
||||
; itself is never force-killed (data loss, e.g. an unsaved deck) so the update
|
||||
; is aborted instead, while the console applications Oracle and Servatrice are
|
||||
; force-stopped by PID once the grace period is over. On interactive installs
|
||||
; the user is prompted to close them instead.
|
||||
; Only needed this early for /R, where AutoUninstallIfNeeded runs below; for
|
||||
; interactive installs the section re-checks after the directory page, when
|
||||
; $INSTDIR is the directory the user actually picked.
|
||||
${If} $ReinstallMode = 1
|
||||
Call EnsureAppsNotRunning
|
||||
${EndIf}
|
||||
|
||||
${If} $ReinstallMode = 1
|
||||
${AndIf} $PortableMode = 0
|
||||
Call AutoUninstallIfNeeded
|
||||
|
|
@ -144,6 +161,9 @@ ${If} ${NSIS_IS_64_BIT} == 1
|
|||
SetRegView 64
|
||||
${EndIf}
|
||||
|
||||
; Ensure no application instance is still running before removing files.
|
||||
Call un.EnsureAppsNotRunning
|
||||
|
||||
FunctionEnd
|
||||
|
||||
Function RequireAdmin
|
||||
|
|
@ -199,6 +219,473 @@ ${EndIf}
|
|||
|
||||
FunctionEnd
|
||||
|
||||
; --- Running instance handling ---
|
||||
; Cockatrice, Oracle and Servatrice must not be running while files are
|
||||
; replaced or deleted. A still-running process holds locks on its .exe and
|
||||
; Qt runtime DLLs, so a silent upgrade could otherwise end up with a mix of
|
||||
; old and new Qt DLLs next to the new executable, failing with
|
||||
; "The procedure entry point X could not be located in the dynamic link
|
||||
; library ...Qt6Network.dll" on the next start.
|
||||
;
|
||||
; On a silent update (/R /S) matching processes are asked to close gracefully
|
||||
; (WM_CLOSE) and waited for up to 60 s. Cockatrice is never force-stopped: it
|
||||
; may be holding an unsaved deck or showing a "really leave this game?"
|
||||
; confirmation, so if it refuses to close the install is aborted - nothing has
|
||||
; been replaced yet, so the update is simply deferred. Oracle and Servatrice
|
||||
; are console applications without a main window, so CloseMainWindow() does
|
||||
; nothing for them and waiting cannot help; they hold no unsaved user data and
|
||||
; are force-stopped by PID once the grace period is over. On interactive
|
||||
; installs and uninstalls the user is prompted to close them instead.
|
||||
;
|
||||
; Processes are matched by image name AND by their executable path living
|
||||
; under the guarded directory ($GuardDir, normally $INSTDIR), so unrelated
|
||||
; processes that merely share an image name (e.g. the Oracle DB instance
|
||||
; "oracle.exe") are never touched.
|
||||
|
||||
; usage: set $R2 to the base image name (without extension, e.g. "cockatrice",
|
||||
; as accepted by Get-Process -Name), call this, result in $R0
|
||||
; (1 = running from $GuardDir, 0 = not running, 2 = could not determine)
|
||||
Function IsAppRunning
|
||||
; Every caller has to set $GuardDir; an empty one would make the path check
|
||||
; below match every process that merely shares the image name, which is the
|
||||
; opposite of what this guard is for.
|
||||
${If} $GuardDir == ""
|
||||
StrCpy $R0 2
|
||||
Return
|
||||
${EndIf}
|
||||
; Pass the directory to guard to PowerShell via an environment variable so
|
||||
; it never has to be embedded into the -Command string: apostrophes in a
|
||||
; path would terminate the command early, and the path in a -like pattern
|
||||
; would treat [ and ] as wildcard characters.
|
||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||
; Exit the process explicitly so nsExec's pop reflects whether a matching
|
||||
; process was found: powershell.exe exits 0 whether or not the pipeline
|
||||
; produced objects. 0 = not running, 1 = running, anything else = could not
|
||||
; be determined (see IsProcessLookupAvailable).
|
||||
; A process is only a match when its path is readable and lives under
|
||||
; $GuardDir. A process we cannot inspect is not one of ours: the updater is
|
||||
; always started by the application it updates, so it runs in the same user
|
||||
; session, and an interactive install is elevated and can read the paths of
|
||||
; the user's own processes. Assuming a match instead would be a hard stop
|
||||
; rather than a fail-closed guard - an Oracle database instance running as
|
||||
; SYSTEM has a process named "oracle" whose path raises Access denied, and
|
||||
; that would make every silent update abort and every interactive install
|
||||
; loop on the Retry prompt with no way out but Cancel.
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"'
|
||||
Pop $R0
|
||||
${If} $R0 == "0"
|
||||
StrCpy $R0 0
|
||||
${ElseIf} $R0 == "1"
|
||||
StrCpy $R0 1
|
||||
${Else}
|
||||
; powershell.exe could not be run at all (nsExec pushes the string
|
||||
; "error"), or died with an unexpected code. That is "could not
|
||||
; determine", which has to stay distinguishable from "definitely
|
||||
; running": a state that can never clear would block the installer
|
||||
; forever. IsProcessLookupAvailable warns about that case once.
|
||||
StrCpy $R0 2
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; Sets $R0 to 1 if PowerShell can be started at all, 0 if it cannot. The
|
||||
; running instance check is built on top of it, so a machine where
|
||||
; powershell.exe is missing, blocked by policy or refused by antivirus is
|
||||
; reported once instead of being reported as "Cockatrice is running" on every
|
||||
; single check.
|
||||
Function IsProcessLookupAvailable
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"'
|
||||
Pop $R0
|
||||
${If} $R0 == "0"
|
||||
StrCpy $R0 1
|
||||
${Else}
|
||||
StrCpy $R0 0
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function CloseMatchingApps
|
||||
; gracefully ask every matching instance to close (sends WM_CLOSE)
|
||||
;
|
||||
; The filter here is deliberately the strict one: a process is only asked to
|
||||
; close when its path is readable and lives under $GuardDir. Treating a
|
||||
; process whose path cannot be read (an unrelated oracle.exe / servatrice.exe,
|
||||
; or one owned by another account running as SYSTEM) as a match would make
|
||||
; the installer close a third party application, and it runs elevated, so
|
||||
; CloseMainWindow() would succeed on it.
|
||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
|
||||
Pop $R3
|
||||
FunctionEnd
|
||||
|
||||
Function ForceCloseApps
|
||||
; stop the matching instances by PID once the grace period is over. Only
|
||||
; used for the console applications ($AllowForceClose), which have no window
|
||||
; to close and hold no unsaved user data. Same strict filter as above.
|
||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"'
|
||||
Pop $R3
|
||||
FunctionEnd
|
||||
|
||||
Function WaitForAppToClose
|
||||
; usage: set $R1 to the display name (e.g. "cockatrice.exe") and $R2 to the
|
||||
; base image name (e.g. "cockatrice")
|
||||
Call IsAppRunning
|
||||
; Only a definite "running" is worth waiting for. "Not running" is done, and
|
||||
; "could not determine" must not keep the installer waiting on an answer it
|
||||
; will never get - EnsureAppsNotRunning has warned about that case already.
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
${If} ${Silent}
|
||||
; ask the application to close gracefully (WM_CLOSE), then wait for it to exit
|
||||
DetailPrint "Closing $R1 ..."
|
||||
Call CloseMatchingApps
|
||||
; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so
|
||||
; a bare loop counter would not bound the real wait time to 60 s.
|
||||
System::Call 'kernel32::GetTickCount()i.s'
|
||||
Pop $R8
|
||||
ck_wait_loop:
|
||||
Sleep 500
|
||||
Call IsAppRunning
|
||||
${If} $R0 = 0
|
||||
DetailPrint "$R1 closed."
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
System::Call 'kernel32::GetTickCount()i.s'
|
||||
Pop $R9
|
||||
IntOp $R9 $R9 - $R8
|
||||
${If} $R9 < 60000
|
||||
Goto ck_wait_loop
|
||||
${EndIf}
|
||||
; The grace period is over. Cockatrice is never force-stopped: it may be
|
||||
; showing a "really leave this game?" confirmation or holding an unsaved
|
||||
; deck, and nothing has been replaced yet, so aborting just defers the
|
||||
; update and preserves the user's data.
|
||||
${If} $AllowForceClose = 0
|
||||
DetailPrint "$R1 is still running after 60 s; aborting the update so that no data is lost."
|
||||
; The in-app updater runs this installer silent and detached and
|
||||
; never looks at its exit code, so a plain Abort would leave the user
|
||||
; with no update and no explanation. A /S install without /R on the
|
||||
; other hand is unattended - by a package manager, for instance -
|
||||
; and a message box there would block forever, so that case only
|
||||
; reports through the log and the exit code.
|
||||
${If} $ReinstallMode = 1
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||
"$R1 did not close within 60 seconds, so the installation was aborted to avoid losing unsaved data.$\r$\n$\r$\nPlease close $R1 and run the installer again."
|
||||
${EndIf}
|
||||
SetErrorLevel 1
|
||||
Abort
|
||||
${EndIf}
|
||||
; Oracle and Servatrice are console applications: CloseMainWindow()
|
||||
; returned false for them and waiting longer cannot help. They hold no
|
||||
; unsaved user data, so stop them by PID.
|
||||
DetailPrint "Force-stopping $R1 ..."
|
||||
Call ForceCloseApps
|
||||
Sleep 2000
|
||||
Call IsAppRunning
|
||||
${If} $R0 = 0
|
||||
DetailPrint "$R1 closed."
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
DetailPrint "$R1 is still running after 60 s; aborting the update."
|
||||
${If} $ReinstallMode = 1
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||
"$R1 could not be stopped, so the installation was aborted.$\r$\n$\r$\nPlease close $R1 and run the installer again."
|
||||
${EndIf}
|
||||
SetErrorLevel 1
|
||||
Abort
|
||||
${Else}
|
||||
ck_wait_prompt:
|
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
||||
"$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \
|
||||
IDCANCEL ck_abort_install
|
||||
Call IsAppRunning
|
||||
${If} $R0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
Goto ck_wait_prompt
|
||||
ck_abort_install:
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function EnsureAppsNotRunning
|
||||
; guards the install directory
|
||||
StrCpy $GuardDir $INSTDIR
|
||||
Call GuardAppsNotRunning
|
||||
FunctionEnd
|
||||
|
||||
Function EnsureOldInstallNotRunning
|
||||
; Guards the directory of a *previous* installation, as recorded in the
|
||||
; registry. Needed by componentsPagePre, which runs the old uninstaller
|
||||
; before the directory page, so $INSTDIR is still the default there and not
|
||||
; the directory the old install lives in. Every uninstaller shipped before
|
||||
; this change had no running instance guard of its own, so the old one would
|
||||
; delete files while an instance of the application is still holding them.
|
||||
ReadRegStr $R3 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "InstallLocation"
|
||||
${If} $R3 == ""
|
||||
; no usable record of where the old install is; $INSTDIR is the default
|
||||
; install directory at this point, which is where it usually is anyway
|
||||
DetailPrint "Could not read the previous installation directory from the registry; checking $INSTDIR instead."
|
||||
StrCpy $R3 $INSTDIR
|
||||
${EndIf}
|
||||
StrCpy $GuardDir $R3
|
||||
Call GuardAppsNotRunning
|
||||
FunctionEnd
|
||||
|
||||
Function GuardAppsNotRunning
|
||||
; usage: set $GuardDir to the directory to guard, then call this
|
||||
Call IsProcessLookupAvailable
|
||||
${If} $R0 = 0
|
||||
; Without PowerShell we cannot tell whether a running instance is
|
||||
; holding locks on the files we are about to replace. Warn once and
|
||||
; continue: refusing to install would leave the user with an installer
|
||||
; that can never complete, which is worse than the locked-file failure
|
||||
; this check exists to prevent. MessageBox is shown even in a silent
|
||||
; install - the user has to be told to close the applications by hand.
|
||||
DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the installation reports a problem."
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION "This installer could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
|
||||
Return
|
||||
${EndIf}
|
||||
StrCpy $R1 "cockatrice.exe"
|
||||
StrCpy $R2 "cockatrice"
|
||||
; a GUI application with unsaved user data: graceful close only
|
||||
StrCpy $AllowForceClose 0
|
||||
Call WaitForAppToClose
|
||||
StrCpy $R1 "oracle.exe"
|
||||
StrCpy $R2 "oracle"
|
||||
StrCpy $AllowForceClose 1
|
||||
Call WaitForAppToClose
|
||||
StrCpy $R1 "servatrice.exe"
|
||||
StrCpy $R2 "servatrice"
|
||||
StrCpy $AllowForceClose 1
|
||||
Call WaitForAppToClose
|
||||
FunctionEnd
|
||||
|
||||
; Sets $R0 to 1 if $INSTDIR looks like an existing Cockatrice install. Used to
|
||||
; guard the stale-runtime-DLL purge so a first-time install into a pre-existing
|
||||
; directory with an unrelated "Plugins" folder is never touched, while an
|
||||
; install that only ever held e.g. servatrice.exe is still purged.
|
||||
Function IsExistingCockatriceInstall
|
||||
StrCpy $R0 0
|
||||
${If} ${FileExists} "$INSTDIR\cockatrice.exe"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$INSTDIR\oracle.exe"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$INSTDIR\servatrice.exe"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$INSTDIR\uninstall.exe"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
; A leftover runtime DLL counts as an install as well. A previously failed
|
||||
; update can leave a directory in which the old uninstaller removed the
|
||||
; executables and uninstall.exe but silently skipped the DLLs its running
|
||||
; instance had locked - which is exactly the broken install the purge is
|
||||
; meant to heal, and without these markers none of the files above would be
|
||||
; there to prove that it is one.
|
||||
${If} ${FileExists} "$INSTDIR\Qt6Core.dll"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$INSTDIR\qt.conf"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; Sets $R0 to 1 if a runtime file of a previous version is still present after
|
||||
; the purge. NSIS silently skips whatever it cannot delete, so one surviving DLL
|
||||
; is enough to prove that something still holds a lock on the install
|
||||
; directory. The purge works on wildcards, which the file existence checks do
|
||||
; not, so this looks for files that are certainly part of the payload.
|
||||
Function HasStaleRuntimeFiles
|
||||
StrCpy $R0 0
|
||||
${If} ${FileExists} "$INSTDIR\Qt6Core.dll"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
${If} ${FileExists} "$INSTDIR\Qt6Network.dll"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
; Uninstaller copies of the same routines (the uninstaller gets its own
|
||||
; function set compiled in, it cannot call the installer functions).
|
||||
Function un.IsAppRunning
|
||||
; An unset $GuardDir would match every process that merely shares the image
|
||||
; name, see IsAppRunning in the installer.
|
||||
${If} $GuardDir == ""
|
||||
StrCpy $R0 2
|
||||
Return
|
||||
${EndIf}
|
||||
; Pass the directory to guard to PowerShell via an environment variable so
|
||||
; it never has to be embedded into the -Command string: apostrophes in a
|
||||
; path would terminate the command early, and the path in a -like pattern
|
||||
; would treat [ and ] as wildcard characters.
|
||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||
; Exit the process explicitly so nsExec's pop reflects whether a matching
|
||||
; process was found: powershell.exe exits 0 whether or not the pipeline
|
||||
; produced objects. 0 = not running, 1 = running, anything else = could not
|
||||
; be determined (see un.IsProcessLookupAvailable).
|
||||
; Strict match: a readable path under $GuardDir, same as in the installer.
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "if (Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | Select-Object -First 1) { exit 1 } else { exit 0 }"'
|
||||
Pop $R0
|
||||
${If} $R0 == "0"
|
||||
StrCpy $R0 0
|
||||
${ElseIf} $R0 == "1"
|
||||
StrCpy $R0 1
|
||||
${Else}
|
||||
; powershell.exe could not be run at all (nsExec pushes the string
|
||||
; "error"), or died with an unexpected code. That is "could not
|
||||
; determine", which has to stay distinguishable from "definitely
|
||||
; running": a state that can never clear would block the uninstaller
|
||||
; forever. un.IsProcessLookupAvailable warns about that case once.
|
||||
StrCpy $R0 2
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function un.IsProcessLookupAvailable
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "exit 0"'
|
||||
Pop $R0
|
||||
${If} $R0 == "0"
|
||||
StrCpy $R0 1
|
||||
${Else}
|
||||
StrCpy $R0 0
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function un.CloseMatchingApps
|
||||
; gracefully ask every matching instance to close (sends WM_CLOSE)
|
||||
;
|
||||
; Strict filter, same as in the installer: never close a process whose path
|
||||
; cannot be read, it is not necessarily one of ours.
|
||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { $$null = $$_.CloseMainWindow() }"'
|
||||
Pop $R3
|
||||
FunctionEnd
|
||||
|
||||
Function un.ForceCloseApps
|
||||
; stop the matching instances by PID once the grace period is over, see
|
||||
; ForceCloseApps in the installer.
|
||||
System::Call 'kernel32::SetEnvironmentVariable(t, t) i ("COCKATRICE_GUARD_DIR", "$GuardDir")'
|
||||
nsExec::ExecToLog 'powershell -NoProfile -Command "Get-Process -Name $\'$R2$\' -ErrorAction SilentlyContinue | Where-Object { try { $$p = $$_.Path; if ($$p) { $$p.StartsWith($$env:COCKATRICE_GUARD_DIR, [StringComparison]::OrdinalIgnoreCase) } else { $$false } } catch { $$false } } | ForEach-Object { Stop-Process -Id $$_.Id -Force -ErrorAction SilentlyContinue }"'
|
||||
Pop $R3
|
||||
FunctionEnd
|
||||
|
||||
Function un.WaitForAppToClose
|
||||
Call un.IsAppRunning
|
||||
; Only a definite "running" is worth waiting for, see WaitForAppToClose.
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
${If} ${Silent}
|
||||
DetailPrint "Closing $R1 ..."
|
||||
Call un.CloseMatchingApps
|
||||
; wall-clock deadline: each poll has to spawn a fresh powershell.exe, so
|
||||
; a bare loop counter would not bound the real wait time to 60 s.
|
||||
System::Call 'kernel32::GetTickCount()i.s'
|
||||
Pop $R8
|
||||
un_ck_wait_loop:
|
||||
Sleep 500
|
||||
Call un.IsAppRunning
|
||||
${If} $R0 = 0
|
||||
DetailPrint "$R1 closed."
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
System::Call 'kernel32::GetTickCount()i.s'
|
||||
Pop $R9
|
||||
IntOp $R9 $R9 - $R8
|
||||
${If} $R9 < 60000
|
||||
Goto un_ck_wait_loop
|
||||
${EndIf}
|
||||
; The grace period is over. Cockatrice is never force-stopped, see
|
||||
; WaitForAppToClose in the installer. A silent uninstall only reports
|
||||
; through the log and its exit code: it is also run unattended (the /R
|
||||
; updater uses it via QuietUninstallString, package managers use it
|
||||
; directly), and a message box would block there. The installer that ran
|
||||
; us reports the incomplete result to the user itself.
|
||||
${If} $AllowForceClose = 0
|
||||
DetailPrint "$R1 is still running after 60 s; aborting so that no data is lost."
|
||||
SetErrorLevel 1
|
||||
Abort
|
||||
${EndIf}
|
||||
DetailPrint "Force-stopping $R1 ..."
|
||||
Call un.ForceCloseApps
|
||||
Sleep 2000
|
||||
Call un.IsAppRunning
|
||||
${If} $R0 = 0
|
||||
DetailPrint "$R1 closed."
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
DetailPrint "$R1 is still running after 60 s; aborting."
|
||||
SetErrorLevel 1
|
||||
Abort
|
||||
${Else}
|
||||
un_ck_wait_prompt:
|
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION|MB_DEFBUTTON1 \
|
||||
"$R1 is still running.$\r$\n$\r$\nPlease close it, then click Retry.$\r$\nClick Cancel to abort." \
|
||||
IDCANCEL un_ck_abort_install
|
||||
Call un.IsAppRunning
|
||||
${If} $R0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $R0 != 1
|
||||
Return
|
||||
${EndIf}
|
||||
Goto un_ck_wait_prompt
|
||||
un_ck_abort_install:
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function un.EnsureAppsNotRunning
|
||||
; guards the install directory; the uninstaller only ever has to check this one
|
||||
StrCpy $GuardDir $INSTDIR
|
||||
Call un.GuardAppsNotRunning
|
||||
FunctionEnd
|
||||
|
||||
Function un.GuardAppsNotRunning
|
||||
Call un.IsProcessLookupAvailable
|
||||
${If} $R0 = 0
|
||||
; Same reasoning as in the installer: without PowerShell we cannot tell
|
||||
; whether a running instance still holds file locks, and refusing to
|
||||
; uninstall would leave the user with an uninstaller that never
|
||||
; completes. Warn once and continue.
|
||||
DetailPrint "Could not start powershell.exe; skipping the running instance check. Close Cockatrice, Oracle and Servatrice manually if the uninstall reports a problem."
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION "This uninstaller could not check whether Cockatrice is still running, because powershell.exe could not be started.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice manually before continuing."
|
||||
Return
|
||||
${EndIf}
|
||||
StrCpy $R1 "cockatrice.exe"
|
||||
StrCpy $R2 "cockatrice"
|
||||
; a GUI application with unsaved user data: graceful close only
|
||||
StrCpy $AllowForceClose 0
|
||||
Call un.WaitForAppToClose
|
||||
StrCpy $R1 "oracle.exe"
|
||||
StrCpy $R2 "oracle"
|
||||
StrCpy $AllowForceClose 1
|
||||
Call un.WaitForAppToClose
|
||||
StrCpy $R1 "servatrice.exe"
|
||||
StrCpy $R2 "servatrice"
|
||||
StrCpy $AllowForceClose 1
|
||||
Call un.WaitForAppToClose
|
||||
FunctionEnd
|
||||
|
||||
Function PortableModePageCreate
|
||||
|
||||
${If} $ReinstallMode = 1
|
||||
|
|
@ -258,8 +745,15 @@ ${If} $PortableMode = 0
|
|||
${EndIf}
|
||||
|
||||
uninst32:
|
||||
; the old uninstaller is run here, before the directory page, and every
|
||||
; uninstaller predating this change has no guard of its own: make sure no
|
||||
; instance of the application is still running out of the *old* install
|
||||
; directory before letting it delete anything. The uninstall string is
|
||||
; stashed away first, the check works on $R0.
|
||||
StrCpy $OldUninstallString $R0
|
||||
Call EnsureOldInstallNotRunning
|
||||
ClearErrors
|
||||
ExecWait "$R0"
|
||||
ExecWait "$OldUninstallString"
|
||||
|
||||
done32:
|
||||
|
||||
|
|
@ -278,8 +772,10 @@ ${If} $PortableMode = 0
|
|||
${EndIf}
|
||||
|
||||
uninst64:
|
||||
StrCpy $OldUninstallString $R0
|
||||
Call EnsureOldInstallNotRunning
|
||||
ClearErrors
|
||||
ExecWait "$R0"
|
||||
ExecWait "$OldUninstallString"
|
||||
|
||||
done64:
|
||||
${EndIf}
|
||||
|
|
@ -294,6 +790,26 @@ Section "Application" SecApplication
|
|||
SetShellVarContext all
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
; No application instance may be running while any file below is deleted or
|
||||
; replaced: both upgrade blocks start by removing the old executables and
|
||||
; runtime DLLs, and NSIS silently skips whatever is locked, which is how an
|
||||
; install ends up with a mix of old and new Qt DLLs next to the new executable
|
||||
; ("The procedure entry point X could not be located in the dynamic link
|
||||
; library ...Qt6Network.dll"). So the check has to come before the first
|
||||
; Delete/RMDir and not after it - an interactive install into an existing
|
||||
; portable directory with cockatrice.exe still running would otherwise delete
|
||||
; the old files under its feet.
|
||||
;
|
||||
; It is skipped on the /R path: .onInit already ran it before
|
||||
; AutoUninstallIfNeeded removed the previous installation, so by the time this
|
||||
; section starts the old files are gone and a second Abort here would leave the
|
||||
; user with the old version uninstalled and the new one not installed at all,
|
||||
; which is worse than the locked files being prevented. The leftover runtime
|
||||
; files are reported at the end of the purge instead.
|
||||
${If} $ReinstallMode = 0
|
||||
Call EnsureAppsNotRunning
|
||||
${EndIf}
|
||||
|
||||
${If} $PortableMode = 1
|
||||
${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
||||
; upgrade portable mode
|
||||
|
|
@ -308,6 +824,8 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
|||
Delete "$INSTDIR\Qt*.dll"
|
||||
Delete "$INSTDIR\libmysql.dll"
|
||||
Delete "$INSTDIR\icu*.dll"
|
||||
Delete "$INSTDIR\libcrypto*.dll"
|
||||
Delete "$INSTDIR\libssl*.dll"
|
||||
Delete "$INSTDIR\libeay32.dll"
|
||||
Delete "$INSTDIR\ssleay32.dll"
|
||||
Delete "$INSTDIR\qt.conf"
|
||||
|
|
@ -318,6 +836,35 @@ ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
|
|||
RMDir "$INSTDIR"
|
||||
${EndIf}
|
||||
|
||||
; Remove any runtime DLLs left over from older versions, so a mismatched
|
||||
; Qt/OpenSSL set cannot survive next to the new executables.
|
||||
Call IsExistingCockatriceInstall
|
||||
${If} $PortableMode = 0
|
||||
${AndIf} $R0 = 1
|
||||
RMDir /r "$INSTDIR\Plugins"
|
||||
Delete "$INSTDIR\Qt*.dll"
|
||||
Delete "$INSTDIR\libcrypto*.dll"
|
||||
Delete "$INSTDIR\libssl*.dll"
|
||||
Delete "$INSTDIR\zlib*.dll"
|
||||
Delete "$INSTDIR\libmysql.dll"
|
||||
Delete "$INSTDIR\icu*.dll"
|
||||
Delete "$INSTDIR\libeay32.dll"
|
||||
Delete "$INSTDIR\ssleay32.dll"
|
||||
${EndIf}
|
||||
|
||||
; Whatever was locked during the purge - by an instance that started while the
|
||||
; installer was running, or because the running instance check could not be
|
||||
; performed at all - is still on disk, and the new executables are about to be
|
||||
; written next to it. That mix is exactly what makes the next start fail with
|
||||
; "The procedure entry point X could not be located in the dynamic link library
|
||||
; ...Qt6Network.dll", so say so instead of leaving it to the user to find out.
|
||||
Call HasStaleRuntimeFiles
|
||||
${If} $R0 = 1
|
||||
DetailPrint "Files of a previous version are still present; the installation may be incomplete."
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||
"The installation could not remove all files of a previous version of Cockatrice, most likely because it is still running or was started as administrator.$\r$\n$\r$\nPlease close Cockatrice, Oracle and Servatrice and run the installer again."
|
||||
${EndIf}
|
||||
|
||||
@CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS@
|
||||
@CPACK_NSIS_FULL_INSTALL@
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "dlg_move_top_cards_until.h"
|
||||
|
||||
#include <QCompleter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
|
|
@ -19,6 +20,7 @@ DlgMoveTopCardsUntil::DlgMoveTopCardsUntil(QWidget *parent, const MoveTopCardsUn
|
|||
exprComboBox = new QComboBox(this);
|
||||
exprComboBox->setFocus();
|
||||
exprComboBox->setEditable(true);
|
||||
exprComboBox->completer()->setCaseSensitivity(Qt::CaseSensitive);
|
||||
exprComboBox->setInsertPolicy(QComboBox::InsertAtTop);
|
||||
exprComboBox->insertItems(0, options.exprs);
|
||||
exprLabel->setBuddy(exprComboBox);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
|
|||
|
||||
textLabel = new QLabel(tr("To change your avatar, choose a new image.\nTo remove your current avatar, confirm "
|
||||
"without choosing a new image."));
|
||||
hintLabel = new QLabel(
|
||||
tr("Images are automatically downscaled to at most %1x%1 pixels.").arg(QString::number(MAX_AVATAR_DIMENSION)));
|
||||
|
||||
browseButton = new QPushButton(tr("Browse..."));
|
||||
connect(browseButton, &QPushButton::clicked, this, &DlgEditAvatar::actBrowse);
|
||||
|
||||
|
|
@ -26,6 +29,7 @@ DlgEditAvatar::DlgEditAvatar(QWidget *parent) : QDialog(parent), image()
|
|||
grid->addWidget(imageLabel, 0, 0, 1, 2);
|
||||
grid->addWidget(textLabel, 1, 0);
|
||||
grid->addWidget(browseButton, 1, 1);
|
||||
grid->addWidget(hintLabel, 2, 0, 1, 2);
|
||||
|
||||
auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &DlgEditAvatar::actOk);
|
||||
|
|
@ -72,6 +76,10 @@ QByteArray DlgEditAvatar::getImage()
|
|||
return QByteArray();
|
||||
}
|
||||
|
||||
if (image.width() > MAX_AVATAR_DIMENSION || image.height() > MAX_AVATAR_DIMENSION) {
|
||||
image = image.scaled(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
QByteArray ba;
|
||||
QBuffer buffer(&ba);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ private slots:
|
|||
void actBrowse();
|
||||
|
||||
private:
|
||||
static constexpr int MAX_AVATAR_DIMENSION = 1024;
|
||||
QImage image;
|
||||
QLabel *textLabel, *imageLabel;
|
||||
QLabel *textLabel, *imageLabel, *hintLabel;
|
||||
QPushButton *browseButton;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,25 @@
|
|||
#include "../client/network/update/client/release_channel.h"
|
||||
#include "../interface/window_main.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtNetwork>
|
||||
#include <version_string.h>
|
||||
|
||||
// Executable that, when it sits next to the downloaded update installer, is installed instead of
|
||||
// it. A packager shipping a custom build - or someone testing one - can drop the file there and
|
||||
// have Cockatrice run it rather than the official installer, without the release channel having to
|
||||
// host an installer for that build. It is a drop-in and is run with the same arguments.
|
||||
static const QString UPDATE_INSTALLER_OVERRIDE = "Cockatrice-Update-Override.exe";
|
||||
|
||||
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
|
||||
|
|
@ -203,6 +213,24 @@ void DlgUpdate::setLabel(const QString &newText)
|
|||
statusLabel->setText(newText);
|
||||
}
|
||||
|
||||
void DlgUpdate::warnInstallerIsWaiting()
|
||||
{
|
||||
// Modeless on purpose: the installer asks the application to close exactly once (see
|
||||
// CloseMatchingApps in NSIS.template.in) and only polls afterwards, while Qt drops spontaneous
|
||||
// close events - a WM_CLOSE from the installer - for windows that are blocked by a modal
|
||||
// widget (QGuiApplicationPrivate::processCloseEvent). An application modal message box here
|
||||
// would therefore swallow the installer's one and only request for the rest of its wait.
|
||||
auto *notification = new QMessageBox(QMessageBox::Warning, tr("Update"),
|
||||
tr("The update installer is already running and waits about a "
|
||||
"minute for Cockatrice to close. Cockatrice is still busy, so "
|
||||
"save your work and close it before then. Otherwise the "
|
||||
"installer gives up and the update is cancelled."),
|
||||
QMessageBox::Ok, parentWidget());
|
||||
notification->setWindowModality(Qt::NonModal);
|
||||
notification->setAttribute(Qt::WA_DeleteOnClose);
|
||||
notification->show();
|
||||
}
|
||||
|
||||
void DlgUpdate::updateCheckError(const QString &errorString)
|
||||
{
|
||||
setLabel(tr("Error"));
|
||||
|
|
@ -224,6 +252,12 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
|
|||
|
||||
QString installerPath = filepath.toLocalFile();
|
||||
|
||||
const QString overridePath = QDir(QFileInfo(installerPath).absolutePath()).filePath(UPDATE_INSTALLER_OVERRIDE);
|
||||
if (QFileInfo::exists(overridePath)) {
|
||||
qCInfo(DlgUpdateLog) << "Installing the update installer override instead of the download:" << overridePath;
|
||||
installerPath = overridePath;
|
||||
}
|
||||
|
||||
QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath());
|
||||
QProcess process;
|
||||
process.setProgram(installerPath);
|
||||
|
|
@ -240,8 +274,31 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
|
|||
|
||||
// Try to open the installer. If it opens, quit Cockatrice
|
||||
if (process.startDetached()) {
|
||||
QMetaObject::invokeMethod(static_cast<MainWindow *>(parent()), "close", Qt::QueuedConnection);
|
||||
qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice";
|
||||
qCInfo(DlgUpdateLog) << "Opened update installer successfully - closing Cockatrice";
|
||||
// Close the main window synchronously so file locks are released before the NSIS installer
|
||||
// (already launched) starts replacing files. This also flushes settings and shuts down the
|
||||
// tabs, but only when the close is actually accepted: MainWindow may veto it for a running
|
||||
// card DB update, an open game, or an unsaved deck, and closeForUpdate() also reports a
|
||||
// close already in progress (reached from a nested event loop while a shutdown prompt is
|
||||
// up). Only quit when the shutdown really ran - otherwise keep running so the user can
|
||||
// resolve the blocker, and warn them that the installer only waits about a minute
|
||||
// before it gives up and cancels the update.
|
||||
if (auto *window = qobject_cast<MainWindow *>(parent())) {
|
||||
if (window->closeForUpdate()) {
|
||||
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
|
||||
} else {
|
||||
warnInstallerIsWaiting();
|
||||
}
|
||||
} else {
|
||||
// Not a MainWindow, so no faithful close can be requested - but the parent still gets
|
||||
// its close() call, which is what the code before this change did and lets it flush
|
||||
// settings and shut down its tabs. Leaving is then unconditional: the installer is
|
||||
// already running against a process that still holds locks on the files it replaces.
|
||||
if (auto *widget = parentWidget()) {
|
||||
widget->close();
|
||||
}
|
||||
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
|
||||
}
|
||||
close();
|
||||
} else {
|
||||
setLabel(tr("Error"));
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ private:
|
|||
void addStopDownloadAndRemoveOthers(bool enable);
|
||||
void beginUpdateCheck();
|
||||
void setLabel(const QString &text);
|
||||
void warnInstallerIsWaiting();
|
||||
QLabel *statusLabel, *descriptionLabel;
|
||||
QProgressBar *progress;
|
||||
QPushButton *manualDownload, *gotoDownload, *ok, *stopDownload;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -49,6 +50,12 @@ namespace
|
|||
// How long to wait after the last visibility change before reading back the
|
||||
// Public/Private column, in milliseconds.
|
||||
constexpr int VISIBILITY_REFRESH_DELAY = 500;
|
||||
|
||||
// Whether the file's name matches one of the deck formats Cockatrice can load.
|
||||
bool isSupportedDeckFile(const QString &filePath)
|
||||
{
|
||||
return QDir::match(DeckLoader::ACCEPTED_FILE_EXTENSIONS, QFileInfo(filePath).fileName());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
||||
|
|
@ -58,6 +65,8 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
|
|||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
localDirModel->setRootPath(SettingsCache::instance().paths().getDeckPath());
|
||||
localDirModel->setNameFilters(DeckLoader::ACCEPTED_FILE_EXTENSIONS);
|
||||
localDirModel->setNameFilterDisables(false);
|
||||
localDirModel->sort(0, Qt::AscendingOrder);
|
||||
|
||||
localDirView = new QTreeView;
|
||||
|
|
@ -312,6 +321,10 @@ void TabDeckStorage::actOpenLocalDeck()
|
|||
}
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
if (!isSupportedDeckFile(filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<LoadedDeck> deckOpt = DeckLoader::loadFromFile(filePath, DeckFileFormat::Cockatrice, true);
|
||||
if (!deckOpt) {
|
||||
continue;
|
||||
|
|
@ -376,6 +389,11 @@ void TabDeckStorage::actUpload()
|
|||
|
||||
void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
|
||||
{
|
||||
if (!isSupportedDeckFile(filePath)) {
|
||||
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
|
||||
return;
|
||||
}
|
||||
|
||||
QFile deckFile(filePath);
|
||||
QFileInfo deckFileInfo(deckFile);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileSystemModel>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
|
|
@ -34,6 +35,12 @@
|
|||
|
||||
inline Q_LOGGING_CATEGORY(TabReplaysLog, "replays_tab");
|
||||
|
||||
namespace
|
||||
{
|
||||
// File name filters for the local replay files Cockatrice can load.
|
||||
const QStringList REPLAY_FILE_NAME_FILTERS = {"*.cor"};
|
||||
} // namespace
|
||||
|
||||
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo)
|
||||
: Tab(_tabSupervisor), client(_client)
|
||||
{
|
||||
|
|
@ -62,6 +69,8 @@ QGroupBox *TabReplays::createLeftLayout()
|
|||
{
|
||||
localDirModel = new QFileSystemModel(this);
|
||||
localDirModel->setRootPath(SettingsCache::instance().paths().getReplaysPath());
|
||||
localDirModel->setNameFilters(REPLAY_FILE_NAME_FILTERS);
|
||||
localDirModel->setNameFilterDisables(false);
|
||||
localDirModel->sort(0, Qt::AscendingOrder);
|
||||
|
||||
localDirView = new QTreeView;
|
||||
|
|
@ -263,6 +272,10 @@ void TabReplays::actOpenLocalReplay()
|
|||
}
|
||||
QString filePath = localDirModel->filePath(curLeft);
|
||||
|
||||
if (!QDir::match(REPLAY_FILE_NAME_FILTERS, QFileInfo(filePath).fileName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QFile f(filePath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -871,7 +871,6 @@ void MainWindow::actShow()
|
|||
void MainWindow::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
// workaround Qt bug where closeEvent gets called twice
|
||||
static bool bClosingDown = false;
|
||||
if (bClosingDown) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -900,6 +899,18 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
|||
tabSupervisor->deleteLater();
|
||||
}
|
||||
|
||||
bool MainWindow::closeForUpdate()
|
||||
{
|
||||
// A shutdown is already being handled (e.g. this is reached from a nested event loop while
|
||||
// closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return
|
||||
// true without any shutdown having happened, so report faithfully instead: the caller must keep
|
||||
// the process alive until the user resolves whatever is blocking the close.
|
||||
if (bClosingDown) {
|
||||
return false;
|
||||
}
|
||||
return close();
|
||||
}
|
||||
|
||||
void MainWindow::changeEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ private:
|
|||
LagMonitor lagMonitor; ///< watches the main thread for event loop stalls
|
||||
LatencyStatusWidget *latencyStatus = nullptr; ///< status bar widget with live round-trip stats and history graph
|
||||
bool bHasActivated, askedForDbUpdater;
|
||||
bool bClosingDown = false; ///< guards closeEvent() against re-entrancy
|
||||
bool skipStartupAutoConnect = false;
|
||||
bool startupAutoConnectAttempted = false;
|
||||
bool firstRunWizardActive = false;
|
||||
|
|
@ -205,6 +206,13 @@ public:
|
|||
return tabSupervisor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Closes the window so an update installer can replace the running binaries.
|
||||
* Returns true only if the shutdown actually ran (settings flushed, tabs shut down);
|
||||
* false if the close was vetoed by the user or is already in progress.
|
||||
*/
|
||||
bool closeForUpdate();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit d52eafe3e9303399fda15661f3d7bb8fe3d7eabc
|
||||
Subproject commit 46483f1e5a70ffb9ecd3b82d0a1cd1b24edf13da
|
||||
|
|
@ -982,6 +982,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Comman
|
|||
return Response::RespInvalidData;
|
||||
}
|
||||
|
||||
if (cmd.deck_list().size() > static_cast<std::string::size_type>(MAX_FILE_LENGTH)) {
|
||||
return Response::RespInvalidData;
|
||||
}
|
||||
|
||||
sqlInterface->checkSql();
|
||||
|
||||
QString deckStr = fileFromStdString(cmd.deck_list());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue