[Client] Improve card database update visibility (#7458)

* [Client] Improve card database update visibility

Addresses Cockatrice/Cockatrice#7431.

Cockatrice previously offered no in-client feedback while the card
database updater ran: the only progress UI lived in the first-run
wizard, stderr/stdout were not parsed in interactive runs, and starting
a second update with Oracle already open or with a hidden background run
active just showed a confusing 'already running' guard - even though
Oracle is launched as a separate process with no lifecycle tracking.

- Add a status-bar widget (CardDatabaseUpdateStatusBar) that appears
  while an update runs and shows the Downloading/Parsing/Importing
  stages with percent progress; it is also used in the quit-confirm
  prompt so users see an unfinished update before cancelling it.
- Parse the updater's PROGRESS lines (new CardUpdateProgressParser,
  unit-tested) in both foreground and background runs and log stage
  changes to the new card_database.update category, throttled to at
  most one line per second; forward Oracle stderr to the logs.
- Show a tray balloon when a background update starts and on finish or
  failure instead of staying silent.
- Request a running Oracle instance to raise itself (QLocalServer
  CockatriceOracleRaise-<user>) when the user starts another update, and
  demote the 'already running' guard to an informative message only when
  no running instance can be found.
- Emit download/scan/import progress unconditionally from Oracle so
  interactive and background runs behave the same.
- Document the new log category in the troubleshooting guide.

* [Client] Address card database update review feedback

* [Client] Add setting to control card database update tray notifications

* [Client] Address remaining card database update review nits

- Rename card_update_progress_parser.{h,cpp} to card_update_progress.{h,cpp}
- Store the CardUpdateProgress struct in the status bar widget

* Lint.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-09 11:07:48 +02:00 • committed by GitHub
parent 6bed5d6f67
commit a44757bd11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 702 additions and 67 deletions

View file

@ -185,9 +185,10 @@ private:
/**
* Whether readSetsFromByteArray() should report scan progress via
* dataReadProgress. A background run routes that signal to stdout (for the
* hosting Cockatrice client to parse); the flag exists to skip the scanner
* instrumentation entirely when no consumer needs it.
* dataReadProgress. The wizard streams that signal to stdout (for the
* hosting Cockatrice client to parse) in both interactive and background
* runs; the flag exists to skip the scanner instrumentation entirely when
* no consumer needs it.
*/
bool progressReporting = true;
@ -255,9 +256,9 @@ public:
* @brief Controls whether readSetsFromByteArray() instruments the raw scan.
*
* When enabled (the default) the raw scanner reports progress via
* dataReadProgress(), which an interactive wizard shows on its progress bar
* and a background run routes to stdout for the hosting client. Switch it
* off only when nothing will consume scan progress.
* dataReadProgress(), which the wizard streams to stdout (so the hosting
* Cockatrice client can show it) and also displays on its own progress bar.
* Switch it off only when nothing will consume scan progress.
*/
void setProgressReporting(bool enabled)
{

View file

@ -14,13 +14,17 @@
#include <QFile>
#include <QIODevice>
#include <QList>
#include <QLocalServer>
#include <QLocalSocket>
#include <QNetworkAccessManager>
#include <QSettings>
#include <QVariant>
#include <QWindow>
#include <QWizardPage>
#include <QtGlobal>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/local_server_name.h>
#include <qlogging.h>
class QWidget;
@ -71,6 +75,7 @@ OracleWizard::OracleWizard(QWidget *parent) : QWizard(parent)
}
retranslateUi();
startRaiseServer();
}
/**
@ -139,6 +144,54 @@ void OracleWizard::runInBackground()
currentPage()->initializePage();
}
void OracleWizard::startRaiseServer()
{
// Scope the socket name to the current user, mirroring the hosting client.
raiseServer = new QLocalServer(this);
connect(raiseServer, &QLocalServer::newConnection, this, &OracleWizard::handleRaiseRequest);
const QString serverName = scopedLocalServerName(QStringLiteral("CockatriceOracleRaise"));
if (raiseServer->listen(serverName)) {
return;
}
// A stale socket left behind by a crashed run holds the name: take it back.
QLocalServer::removeServer(serverName);
if (!raiseServer->listen(serverName)) {
qWarning() << "Oracle: could not listen for raise requests on" << serverName;
delete raiseServer;
raiseServer = nullptr;
}
}
void OracleWizard::handleRaiseRequest()
{
while (raiseServer && raiseServer->hasPendingConnections()) {
QLocalSocket *socket = raiseServer->nextPendingConnection();
connect(socket, &QLocalSocket::readyRead, this, [this, socket]() {
socket->readAll();
socket->deleteLater();
raiseWizard();
});
connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater);
}
raiseWizard();
}
void OracleWizard::raiseWizard()
{
// An ongoing background run is hidden; the user asked to see it, so promote it to a window.
if (backgroundMode && !isVisible()) {
show();
}
if (isMinimized()) {
showNormal();
}
raise();
activateWindow();
if (windowHandle()) {
windowHandle()->requestActivate();
}
}
void OracleWizard::enableButtons()
{
button(QWizard::NextButton)->setDisabled(false);

View file

@ -9,6 +9,7 @@
class QNetworkAccessManager;
class OracleImporter;
class QLocalServer;
class QSettings;
class QWidget;
@ -60,6 +61,7 @@ public:
private slots:
void updateLanguage();
void handleRaiseRequest();
private:
QByteArray tokensData;
@ -67,6 +69,13 @@ private:
QString cardSourceVersion;
void migrateOracleSettings();
/**
* @brief Starts the local socket the hosting Cockatrice client uses to bring this wizard forward.
*/
void startRaiseServer();
/** @brief Shows, raises and activates the wizard window. */
void raiseWizard();
QLocalServer *raiseServer = nullptr; ///< listens for raise requests from the hosting client
protected:
void changeEvent(QEvent *event) override;

View file

@ -82,13 +82,12 @@ class QWidget;
#endif
/**
* @brief Emits one machine-readable background-run progress line to stdout.
* @brief Emits one machine-readable progress line to stdout.
*
* Used only in background mode, so the hosting Cockatrice client can parse these
* lines to drive a determinate progress bar. stderr stays reserved for
* human-readable log output.
* The hosting Cockatrice client parses these lines to drive a determinate
* progress bar. stderr stays reserved for human-readable log output.
*/
static void emitBackgroundProgress(const char *stage, qint64 done, qint64 total)
static void emitProgress(const char *stage, qint64 done, qint64 total)
{
QTextStream out(stdout);
out << "PROGRESS " << stage << ' ' << done << ' ' << total << '\n';
@ -473,9 +472,7 @@ void LoadSetsPage::downloadSetsFile(const QUrl &url)
void LoadSetsPage::actDownloadProgressSetsFile(qint64 received, qint64 total)
{
if (wizard()->backgroundMode) {
emitBackgroundProgress("download", received, total);
}
emitProgress("download", received, total);
if (total > 0) {
progressBar->setMaximum(static_cast<int>(total));
progressBar->setValue(static_cast<int>(received));
@ -534,7 +531,7 @@ void LoadSetsPage::updateParsingProgress(int bytesRead, int totalBytes)
void LoadSetsPage::scanProgressToStdout(int bytesRead, int totalBytes)
{
emitBackgroundProgress("scan", bytesRead, totalBytes);
emitProgress("scan", bytesRead, totalBytes);
}
void LoadSetsPage::beginLoadSets(bool compressedFile)
@ -841,18 +838,18 @@ void SaveSetsPage::updateTotalProgress(int cardsImported, int setIndex, const QS
const int cardCount = wizard()->importer->getCardList().size();
if (wizard()->backgroundMode) {
qInfo() << tr("Import finished: %1 cards.").arg(cardCount);
emitBackgroundProgress("import", totalSets, totalSets);
} else {
messageLog->append("<b>" + tr("Import finished: %1 cards.").arg(cardCount) + "</b>");
}
emitProgress("import", totalSets, totalSets);
} else {
progressBar->setValue(setIndex);
if (wizard()->backgroundMode) {
qInfo() << tr("%1: %2 cards imported").arg(setName).arg(cardsImported);
emitBackgroundProgress("import", setIndex, totalSets);
} else {
messageLog->append(tr("%1: %2 cards imported").arg(setName).arg(cardsImported));
}
emitProgress("import", setIndex, totalSets);
}
messageLog->verticalScrollBar()->setValue(messageLog->verticalScrollBar()->maximum());