[DeckShare] Time the share-list round trip and backstop silently-destroyed intent chains

This commit is contained in:
Lukas Brübach 2026-09-19 07:20:09 +02:00
parent 668b8ab931
commit 116c79802f
4 changed files with 34 additions and 2 deletions

View file

@ -43,7 +43,11 @@ bool IntentOpenSharedDeck::checkPrecondition() const
void IntentOpenSharedDeck::onPreconditionSatisfied() void IntentOpenSharedDeck::onPreconditionSatisfied()
{ {
// Resolve the share token to its items first; a share can contain more than // 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; Command_DeckShareList cmd;
cmd.set_token(context->shareToken.toStdString()); cmd.set_token(context->shareToken.toStdString());
@ -59,6 +63,9 @@ void IntentOpenSharedDeck::onPreconditionNotSatisfied()
void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */) void IntentOpenSharedDeck::listShareFinished(const Response &response, const CommandContainer & /* commandContainer */)
{ {
downloadTimer->stop();
listPhase = false;
if (response.response_code() != Response::RespOk) { if (response.response_code() != Response::RespOk) {
emitFailed(tr("The shared deck could not be found or has expired")); emitFailed(tr("The shared deck could not be found or has expired"));
return; return;
@ -177,6 +184,12 @@ void IntentOpenSharedDeck::onItemFailure(const QString &reason)
void IntentOpenSharedDeck::onDownloadTimeout() 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")); onItemFailure(tr("Timed out while downloading the shared deck"));
} }

View file

@ -51,6 +51,7 @@ private:
QMap<int, QString> itemNames; QMap<int, QString> itemNames;
QList<int> pendingItemIds; QList<int> pendingItemIds;
QList<LoadedDeck> loadedDecks; QList<LoadedDeck> loadedDecks;
bool listPhase = true;
int currentItemId = 0; int currentItemId = 0;
int totalItems = 0; int totalItems = 0;
int completedItems = 0; int completedItems = 0;

View file

@ -313,6 +313,10 @@ void IntentUrlParser::startNextChain()
connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); }); connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); });
connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); }); connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); });
connect(finalIntent, &Intent::cancelled, 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(); chain.intents.first()->execute();
} }
@ -320,6 +324,7 @@ void IntentUrlParser::startNextChain()
void IntentUrlParser::chainEnded(bool chainSucceeded) void IntentUrlParser::chainEnded(bool chainSucceeded)
{ {
chainRunning = false; chainRunning = false;
QObject::disconnect(chainBackstopConnection);
const PendingIntentChain chain = pendingChains.takeFirst(); 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) void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain)
{ {
if (chain.previousServerHost.isEmpty()) { if (chain.previousServerHost.isEmpty()) {

View file

@ -1,5 +1,6 @@
#ifndef COCKATRICE_URL_PARSER_H #ifndef COCKATRICE_URL_PARSER_H
#define COCKATRICE_URL_PARSER_H #define COCKATRICE_URL_PARSER_H
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QUrlQuery> #include <QUrlQuery>
@ -29,7 +30,6 @@ struct PendingIntentChain
QString migrationTargetHost; QString migrationTargetHost;
QString migrationTargetPort; QString migrationTargetPort;
bool pendingRestore = false; bool pendingRestore = false;
bool succeeded = false;
}; };
/** /**
@ -60,12 +60,16 @@ private:
[[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const; [[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const;
void startNextChain(); void startNextChain();
void chainEnded(bool chainSucceeded); void chainEnded(bool chainSucceeded);
void onChainIntentDestroyed();
void restorePreviousServer(const PendingIntentChain &chain); void restorePreviousServer(const PendingIntentChain &chain);
void restoreToPreviousServer(const PendingIntentChain &chain); void restoreToPreviousServer(const PendingIntentChain &chain);
MainWindow *mainWindow; MainWindow *mainWindow;
QList<PendingIntentChain> pendingChains; QList<PendingIntentChain> pendingChains;
bool chainRunning = false; 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 #endif // COCKATRICE_URL_PARSER_H