From 116c79802f4c265237bd099f9a29488be2b8d042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sat, 19 Sep 2026 07:20:09 +0200 Subject: [PATCH] [DeckShare] Time the share-list round trip and backstop silently-destroyed intent chains --- .../interface/intents/intent_open_shared_deck.cpp | 15 ++++++++++++++- .../interface/intents/intent_open_shared_deck.h | 1 + cockatrice/src/interface/intents/url_parser.cpp | 14 ++++++++++++++ cockatrice/src/interface/intents/url_parser.h | 6 +++++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp index ae1d19343..9ecb4520d 100644 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.cpp +++ b/cockatrice/src/interface/intents/intent_open_shared_deck.cpp @@ -43,7 +43,11 @@ bool IntentOpenSharedDeck::checkPrecondition() const void IntentOpenSharedDeck::onPreconditionSatisfied() { // Resolve the share token to its items first; a share can contain more than - // one deck, and each item is downloaded by id. + // one deck, and each item is downloaded by id. Time the round trip like the + // downloads, so a silent server cannot hang the chain forever. + listPhase = true; + downloadTimer->start(); + Command_DeckShareList cmd; cmd.set_token(context->shareToken.toStdString()); @@ -59,6 +63,9 @@ void IntentOpenSharedDeck::onPreconditionNotSatisfied() void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */) { + downloadTimer->stop(); + listPhase = false; + if (response.response_code() != Response::RespOk) { emitFailed(tr("The shared deck could not be found or has expired")); return; @@ -177,6 +184,12 @@ void IntentOpenSharedDeck::onItemFailure(const QString &reason) void IntentOpenSharedDeck::onDownloadTimeout() { + // The list phase has no preview dialog yet to report progress into; fail the + // whole intent instead of letting the shared deck hang in limbo. + if (listPhase) { + emitFailed(tr("Timed out while loading the shared deck")); + return; + } onItemFailure(tr("Timed out while downloading the shared deck")); } diff --git a/cockatrice/src/interface/intents/intent_open_shared_deck.h b/cockatrice/src/interface/intents/intent_open_shared_deck.h index cf7f1b0b1..87812bdea 100644 --- a/cockatrice/src/interface/intents/intent_open_shared_deck.h +++ b/cockatrice/src/interface/intents/intent_open_shared_deck.h @@ -51,6 +51,7 @@ private: QMap itemNames; QList pendingItemIds; QList loadedDecks; + bool listPhase = true; int currentItemId = 0; int totalItems = 0; int completedItems = 0; diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index ee8f755c0..120e72b2a 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -313,6 +313,10 @@ void IntentUrlParser::startNextChain() connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); }); connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); }); connect(finalIntent, &Intent::cancelled, this, [this]() { chainEnded(false); }); + // Backstop: if the final intent is destroyed without emitting a terminal + // signal (e.g. a network error dropped it while running), end the chain so + // later links are not queued and dropped for the rest of the session. + chainBackstopConnection = connect(finalIntent, &QObject::destroyed, this, &IntentUrlParser::onChainIntentDestroyed); chain.intents.first()->execute(); } @@ -320,6 +324,7 @@ void IntentUrlParser::startNextChain() void IntentUrlParser::chainEnded(bool chainSucceeded) { chainRunning = false; + QObject::disconnect(chainBackstopConnection); const PendingIntentChain chain = pendingChains.takeFirst(); @@ -338,6 +343,15 @@ void IntentUrlParser::chainEnded(bool chainSucceeded) } } +void IntentUrlParser::onChainIntentDestroyed() +{ + if (!chainRunning) { + return; + } + qCWarning(UrlParserLog) << "Share-link intent destroyed without a terminal signal; ending its chain"; + chainEnded(false); +} + void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain) { if (chain.previousServerHost.isEmpty()) { diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index cebad0162..ea29fed53 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -1,5 +1,6 @@ #ifndef COCKATRICE_URL_PARSER_H #define COCKATRICE_URL_PARSER_H + #include #include #include @@ -29,7 +30,6 @@ struct PendingIntentChain QString migrationTargetHost; QString migrationTargetPort; bool pendingRestore = false; - bool succeeded = false; }; /** @@ -60,12 +60,16 @@ private: [[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const; void startNextChain(); void chainEnded(bool chainSucceeded); + void onChainIntentDestroyed(); void restorePreviousServer(const PendingIntentChain &chain); void restoreToPreviousServer(const PendingIntentChain &chain); MainWindow *mainWindow; QList pendingChains; bool chainRunning = false; + // Disconnects the destroyed-signal backstop once a chain ends, so an old + // intent's deferred deletion cannot end the chain that runs after it. + QMetaObject::Connection chainBackstopConnection; }; #endif // COCKATRICE_URL_PARSER_H