Cockatrice/cockatrice/src/interface/window_main.h
sentry[bot] bbc7e462ff
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] Graceful close and a way out of a stuck update (#7687)
* [Installer] Stop calling the payload free while an executable is locked

The guard answered "nothing is holding the payload" from two lookups that can
both miss a running instance: Restart Manager does not reliably report a
program's own image or the modules it mapped, and the image path of a process
running under another account cannot be read from the probe at all. A user log
showed exactly that: the first guard passed while cockatrice.exe was running
from the directory, the old uninstaller silently skipped the file it could not
delete, and the update aborted half an uninstall later with "cancelled by user"
after a close attempt the running window ignored.

Ask the file itself. A running image is opened without write sharing, so the
write open replacing it needs is refused with ERROR_SHARING_VIOLATION for as
long as the program runs, while a file that is merely being read still accepts
it. When neither lookup named anybody and a payload executable refuses that
open, the probe answers that the file is in use and that the program holding it
could not be identified - the guard's "could not confirm" dialog, whose Retry
works once the user has closed it - instead of passing the payload as free.

Only the payload executables are tested, not every file the purge removes: a
locked executable proves a program is running and a mapped DLL belongs to a
running executable, while a locked DLL or qt.conf can be the asking process
itself, which is the loop the guard already had. The open is retried once after
300 ms, and only for the files that refused, so a scanner holding a file for a
moment cannot stop an install for nothing.

The Windows probe check now covers that answer: a payload executable held by
the asking process, so that it cannot be named, has to come back as in use
rather than as free.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>

* [Installer] Close running clients over a local socket

The installer could only ask a running client to close with WM_CLOSE, which an instance hidden in the notification area or sitting on its own confirmation cannot answer usefully. Give every client a per-process local socket and have the holder probe ask it there first, falling back to WM_CLOSE for clients from before the socket.

* [Installer] Offer to check again or update by hand when files stay locked

Replace the two-answer guard prompt with force close, close by hand and check again, or cancel. Retry the automatic close while the set of holders changes, and point a cancelled run at the latest release so a user who does not want anything killed has a way forward that is not cancelling the update again.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 15:32:59 +01:00

274 lines
11 KiB
C++

/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@gmx.net *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
/**
* @file window_main.h
* @ingroup Core
*/
//! \todo Document this file.
#ifndef WINDOW_H
#define WINDOW_H
#include "../client/lag_monitor.h"
#include "../client/network/update/client/release_channel.h"
#include "card_database_update/card_update_progress.h"
#include "connection_controller/remote_connection_controller.h"
#include <QByteArray>
#include <QDir>
#include <QElapsedTimer>
#include <QList>
#include <QLoggingCategory>
#include <QMainWindow>
#include <QMenu>
#include <QProcess>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QtGlobal>
#include <libcockatrice/network/client/abstract/abstract_client.h>
#include <qtmetamacros.h>
class QAction;
class QToolButton;
class QWidget;
struct LocalGameOptions;
inline Q_LOGGING_CATEGORY(WindowMainLog, "window_main");
inline Q_LOGGING_CATEGORY(WindowMainStartupLog, "window_main.startup");
inline Q_LOGGING_CATEGORY(WindowMainStartupVersionLog, "window_main.startup.version");
inline Q_LOGGING_CATEGORY(WindowMainStartupShortcutsLog, "window_main.startup.shortcuts");
inline Q_LOGGING_CATEGORY(WindowMainStartupAutoconnectLog, "window_main.startup.autoconnect");
inline Q_LOGGING_CATEGORY(CardDatabaseUpdateLog, "card_database.update");
class DlgViewLog;
class GameReplay;
class LocalServer;
class LatencyStatusWidget;
class CardDatabaseUpdateStatusBar;
class RemoteClient;
class TabSupervisor;
class WndSets;
class DlgTipOfTheDay;
struct ContextConnectToServer;
class IntentUrlParser;
class MainWindow : public QMainWindow
{
Q_OBJECT
signals:
/** @brief Emitted after the card-database update subprocess exits. */
void cardDatabaseUpdateFinished(bool success);
/** @brief Emitted while the card-database update subprocess runs.
* @p progress.done/@p progress.total are byte counts for the first two stages
* and set indices for "import". */
void cardDatabaseUpdateProgress(const CardUpdateProgress &progress);
public slots:
void actCheckCardUpdates();
void actCheckCardUpdatesBackground();
void actCheckServerUpdates();
void actCheckClientUpdates();
void actConnect();
void actExit();
void handleCockatriceLink(const QString &url);
private slots:
void updateTabMenu(const QList<QMenu *> &newMenuList);
void statusChanged(ClientStatus _status);
void localGameEnded();
void pixmapCacheSizeChanged(int newSizeInMBs);
void actDisconnect();
void actSinglePlayer();
void actWatchReplay();
void actFullScreen(bool checked);
void actSettings();
void actAbout();
void actTips();
void actUpdate();
void actViewLog();
void actOpenSettingsFolder();
void actShow();
void showWindowIfHidden();
void onUrlChainFinished(bool connected);
void cardUpdateError(QProcess::ProcessError err);
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
void cardUpdateProgressOutput();
void cardUpdateStandardError();
void refreshShortcuts();
void cardDatabaseLoadingFailed();
void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);
void cardDatabaseAllNewSetsEnabled();
void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release);
void actCheckCommanderBracketDefinitionUpdates();
void actOpenCustomFolder();
void actOpenCustomsetsFolder();
void actAddCustomSet();
void actReloadCardDatabase();
void actManageSets();
void actEditTokens();
void startupConfigCheck();
void alertForcedOracleRun(const QString &version, bool isUpdate);
void applyStartupDestination();
void onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext);
void startupDestinationFailed(const QString &reason);
[[nodiscard]] bool startupDestinationConnectsToServer() const;
void attemptStartupAutoConnect();
private:
static const QString appName;
static const QStringList fileNameFilters;
void retranslateUi();
void createActions();
void createMenus();
void createTrayIcon();
int getNextCustomSetPrefix(QDir dataDir);
void runFirstRunWizard(bool firstRun = false);
inline QString getCardUpdaterBinaryName()
{
return "oracle";
}
void createCardUpdateProcess(bool background = false);
void exitCardDatabaseUpdate(bool doReload);
/**
* @brief Reloads the card database on a worker thread and runs checkUnknownSets() once the finished
* snapshot has been swapped in.
*
* The loader never runs checkUnknownSets() itself (see main.cpp); post-update loads that can introduce
* new sets must own the check, and it has to run after the queued snapshot swap, not on the worker.
*/
void reloadCardDatabaseAndCheckSets();
/**
* @brief Tries to bring the open card database updater window to the front.
* @return true when the updater acknowledged the request
*/
bool requestCardUpdateRaise();
/** @brief Tells the user an update is running without blocking, honoring a hidden status bar. */
void announceCardUpdateRunning();
/** @brief Posts a tray notification when the status bar cannot show progress. */
void notifyCardUpdateFinishedFromTray(bool success);
void startLocalGame(const LocalGameOptions &options);
QList<QMenu *> tabMenus;
QMenu *cockatriceMenu, *dbMenu, *tabsMenu, *helpMenu, *trayIconMenu;
QAction *aAbout, *aSettings, *aShow, *aExit;
QToolButton *settingsButton = nullptr;
QAction *aConnect, *aDisconnect, *aRegister, *aForgotPassword, *aSinglePlayer, *aWatchReplay, *aFullScreen;
QAction *aManageSets, *aEditTokens, *aOpenCustomFolder, *aOpenCustomsetsFolder, *aAddCustomSet,
*aReloadCardDatabase;
QAction *aTips, *aUpdate, *aCheckCardUpdates, *aCheckCardUpdatesBackground, *aFirstRunWizard, *aStatusBar,
*aViewLog, *aOpenSettingsFolder;
TabSupervisor *tabSupervisor;
IntentUrlParser *urlParser;
WndSets *wndSets;
ConnectionController *connectionController;
LocalServer *localServer;
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;
/**
* How far a shutdown that was asked for has got. A close request that arrives while one is
* already asking the user something must not be accepted - see closeEvent().
*/
enum class ShutdownState
{
Idle, ///< no shutdown has been asked for
Confirming, ///< asking about a card database update, open games or an unsaved deck
Accepted, ///< the close was accepted and the window is hiding
};
ShutdownState shutdownState = ShutdownState::Idle;
bool skipStartupAutoConnect = false;
bool startupAutoConnectAttempted = false;
bool firstRunWizardActive = false;
QProcess *cardUpdateProcess;
QByteArray cardUpdateOutputBuffer;
QByteArray cardUpdateErrorOutput; ///< diagnostic output collected from the updater's stderr
QByteArray cardUpdateErrorPartial; ///< incomplete trailing stderr line, kept for line-based logging
CardUpdateStage cardUpdateLoggedStage = CardUpdateStage::Unknown; ///< stage of the last logged progress update
QElapsedTimer cardUpdateProgressLogTimer; ///< throttles progress logging to the running stage
CardDatabaseUpdateStatusBar *cardUpdateStatusBar = nullptr; ///< status bar widget tracking the card database update
DlgViewLog *logviewDialog;
GameReplay *replay;
DlgTipOfTheDay *tip;
QUrl connectTo;
public:
explicit MainWindow(QWidget *parent = nullptr);
void setConnectTo(QString url)
{
connectTo = QUrl(QString("cockatrice://%1").arg(url));
}
// When set, the window's own startup connection (--connect or auto-connect
// on first activation) is skipped. Used for activation launches: the intent
// chain triggered by a cockatrice:// URL owns the connection, and letting
// auto-connect race against it caused two connectToServer calls to tear
// each other down. onUrlChainFinished() clears this and retries the startup
// connection when the link's chain ended without connecting.
void setSkipStartupAutoConnect(bool skip)
{
skipStartupAutoConnect = skip;
}
~MainWindow() override;
RemoteClient *getRemoteClient() const
{
return connectionController->client();
}
TabSupervisor *getTabSupervisor() const
{
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();
/**
* @brief Closes this instance because the installer asked it to over its local socket.
*
* The request reaches an instance hidden in the notification area or sitting on its own
* confirmation, which is exactly where a WM_CLOSE from the installer does not. The close
* is still the ordinary one: a clean client exits, one with unsaved work raises the same
* confirmation the close button does, and a confirmation already up is brought to the
* front instead of being left behind the installer.
*/
void requestInstallerClose();
protected:
void closeEvent(QCloseEvent *event) override;
void changeEvent(QEvent *event) override;
};
#endif