diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index f9a3fa70a..3d0cd0c7d 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -40,7 +40,7 @@ void IntentUrlParser::handle(const QString &urlStr) qCDebug(UrlParserLog) << "Parsing intent URL, action:" << action; - QList chain; + PendingIntentChain chain; Intent *firstIntent = nullptr; if (action == "joingame") { firstIntent = createJoinGameIntent(query, chain); @@ -64,7 +64,7 @@ void IntentUrlParser::handle(const QString &urlStr) startNextChain(); } -Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, QList &chain) +Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain) { auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open game"), message); }; @@ -116,14 +116,15 @@ Intent *IntentUrlParser::createJoinGameIntent(const QUrlQuery &query, QListgetTabSupervisor(), client, std::move(ctx)); joinGameIntent->setParent(this); - chain.append(joinGameIntent); + chain.intents.append(joinGameIntent); connect(joinGameIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); Intent *firstIntent = joinGameIntent; if (!isConnectedTo(serverContext->hostname, serverContext->port)) { - auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); + auto getLoginCredentialsIntent = + new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); getLoginCredentialsIntent->setParent(joinGameIntent); - chain.insert(0, getLoginCredentialsIntent); + chain.intents.insert(0, getLoginCredentialsIntent); connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); connect(getLoginCredentialsIntent, &Intent::failed, joinGameIntent, &Intent::failed); @@ -169,7 +170,7 @@ QString IntentUrlParser::generateJoinGameMessage(const ContextJoinGame &context, : tr("Join game \"%1\" (#%2) on %3?").arg(gameDescription, gameIdStr, server); } -Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, QList &chain) +Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain) { auto showError = [this](const QString &message) { QMessageBox::warning(mainWindow, tr("Open shared deck"), message); @@ -205,7 +206,8 @@ Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, QListgetStatus() == StatusLoggedIn && !isConnectedTo(ctx->serverContext.hostname, ctx->serverContext.port); if (migrating) { @@ -219,11 +221,9 @@ Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, QListserverContext.hostname; - migrationTargetPort = ctx->serverContext.port; - previousServerHost = client->serverName(); - previousServerPort = QString::number(client->serverPort()); - pendingRestore = true; + chain.migrationTargetHost = ctx->serverContext.hostname; + chain.migrationTargetPort = ctx->serverContext.port; + chain.pendingRestore = true; } ContextConnectToServer *serverContext = &ctx->serverContext; @@ -233,14 +233,15 @@ Intent *IntentUrlParser::createOpenDeckIntent(const QUrlQuery &query, QListgetTabSupervisor(), client, CardDatabaseManager::query(), std::move(ctx)); openDeckIntent->setParent(this); - chain.append(openDeckIntent); + chain.intents.append(openDeckIntent); connect(openDeckIntent, &Intent::failed, this, [showError](const QString &reason) { showError(reason); }); Intent *firstIntent = openDeckIntent; if (!isConnectedTo(serverContext->hostname, serverContext->port)) { - auto getLoginCredentialsIntent = new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); + auto getLoginCredentialsIntent = + new IntentGetLoginCredentials(serverContext, /*promptForMissingCredentials=*/true); getLoginCredentialsIntent->setParent(openDeckIntent); - chain.insert(0, getLoginCredentialsIntent); + chain.intents.insert(0, getLoginCredentialsIntent); connect(getLoginCredentialsIntent, &Intent::finished, openDeckIntent, &Intent::execute); connect(getLoginCredentialsIntent, &Intent::failed, openDeckIntent, &Intent::failed); @@ -267,38 +268,47 @@ void IntentUrlParser::startNextChain() return; } chainRunning = true; - currentChainSucceeded = false; - const QList chain = pendingChains.takeFirst(); - if (chain.isEmpty()) { + PendingIntentChain &chain = pendingChains.first(); + if (chain.intents.isEmpty()) { + pendingChains.removeFirst(); chainRunning = false; + startNextChain(); return; } + // Snapshot the session this chain moves away from now that it actually + // runs. Chains are parsed while earlier ones are still queued, so a capture + // at parse time would follow whichever server the chain before it settled + // on, not the one the user is really on when this link is handled. + if (chain.pendingRestore) { + RemoteClient *client = mainWindow->getRemoteClient(); + chain.previousServerHost = client->serverName(); + chain.previousServerPort = QString::number(client->serverPort()); + } + // Only the last intent completes the chain; its terminal signal ends the // whole run. Cancellation of an intermediate intent (e.g. declined login // prompt) is forwarded onto the last intent in the chain builders above. - Intent *finalIntent = chain.last(); - connect(finalIntent, &Intent::finished, this, [this]() { - currentChainSucceeded = true; - chainEnded(); - }); - connect(finalIntent, &Intent::failed, this, &IntentUrlParser::chainEnded); - connect(finalIntent, &Intent::cancelled, this, &IntentUrlParser::chainEnded); + Intent *finalIntent = chain.intents.last(); + connect(finalIntent, &Intent::finished, this, [this]() { chainEnded(true); }); + connect(finalIntent, &Intent::failed, this, [this]() { chainEnded(false); }); + connect(finalIntent, &Intent::cancelled, this, [this]() { chainEnded(false); }); - chain.first()->execute(); + chain.intents.first()->execute(); } -void IntentUrlParser::chainEnded() +void IntentUrlParser::chainEnded(bool chainSucceeded) { chainRunning = false; + const PendingIntentChain chain = pendingChains.takeFirst(); + // Only a failed or cancelled chain restores the session the link migrated // away from; a successful one leaves the user where they are. - if (pendingRestore && !currentChainSucceeded) { - restorePreviousServer(); + if (chain.pendingRestore && !chainSucceeded) { + restorePreviousServer(chain); } - pendingRestore = false; startNextChain(); @@ -309,9 +319,9 @@ void IntentUrlParser::chainEnded() } } -void IntentUrlParser::restorePreviousServer() +void IntentUrlParser::restorePreviousServer(const PendingIntentChain &chain) { - if (previousServerHost.isEmpty()) { + if (chain.previousServerHost.isEmpty()) { return; } @@ -324,46 +334,47 @@ void IntentUrlParser::restorePreviousServer() // deciding mid-connect would strand the user offline from their previous // server. if (status == StatusDisconnected || status == StatusLoggedIn) { - restoreToPreviousServer(); + restoreToPreviousServer(chain); return; } auto waitConnection = std::make_shared(); - *waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, client, waitConnection]() { + *waitConnection = connect(client, &RemoteClient::statusChanged, this, [this, chain, client, waitConnection]() { const ClientStatus settled = client->getStatus(); if (settled == StatusDisconnected || settled == StatusLoggedIn) { QObject::disconnect(*waitConnection); - restoreToPreviousServer(); + restoreToPreviousServer(chain); } }); } -void IntentUrlParser::restoreToPreviousServer() +void IntentUrlParser::restoreToPreviousServer(const PendingIntentChain &chain) { RemoteClient *client = mainWindow->getRemoteClient(); // Back on the previous server already → nothing to undo. - if (client->serverName().compare(previousServerHost, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == previousServerPort) { + if (client->serverName().compare(chain.previousServerHost, Qt::CaseInsensitive) == 0 && + QString::number(client->serverPort()) == chain.previousServerPort) { return; } // When logged in somewhere, only intervene if that somewhere is the server // the link moved us to; if the user went elsewhere on their own, leave them. if (client->getStatus() == StatusLoggedIn) { - const bool onMigrationTarget = client->serverName().compare(migrationTargetHost, Qt::CaseInsensitive) == 0 && - QString::number(client->serverPort()) == migrationTargetPort; + const bool onMigrationTarget = + client->serverName().compare(chain.migrationTargetHost, Qt::CaseInsensitive) == 0 && + QString::number(client->serverPort()) == chain.migrationTargetPort; if (!onMigrationTarget) { return; } ServersSettings &servers = SettingsCache::instance().servers(); - const int index = servers.findServerIndex(previousServerHost, previousServerPort); - if (index >= 0 && servers.hasLoginData(previousServerHost, previousServerPort)) { + const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); + if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { const QString username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); const QString password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - client->connectToServer(previousServerHost, previousServerPort.toUInt(), username, password); + client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); return; } client->disconnectFromServer(); @@ -377,12 +388,12 @@ void IntentUrlParser::restoreToPreviousServer() // The link's connection attempt failed: reconnect to the previous server // when credentials are saved, otherwise stay offline. ServersSettings &servers = SettingsCache::instance().servers(); - const int index = servers.findServerIndex(previousServerHost, previousServerPort); - if (index >= 0 && servers.hasLoginData(previousServerHost, previousServerPort)) { + const int index = servers.findServerIndex(chain.previousServerHost, chain.previousServerPort); + if (index >= 0 && servers.hasLoginData(chain.previousServerHost, chain.previousServerPort)) { const QString username = servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); const QString password = servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); - client->connectToServer(previousServerHost, previousServerPort.toUInt(), username, password); + client->connectToServer(chain.previousServerHost, chain.previousServerPort.toUInt(), username, password); } } diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index 7464caf4f..cebad0162 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -8,6 +8,30 @@ class Intent; class MainWindow; struct ContextJoinGame; +/** + * @brief One queued intent chain with the session-migration bookkeeping for it. + * + * The restore fields are per-chain on purpose: chains are parsed while earlier + * ones are still queued, so parser-wide state would let one chain's failure + * consume the restore data another chain recorded. + */ +struct PendingIntentChain +{ + QList intents; + + // Snapshot of the session in place when this chain started running, so a + // queued chain follows whichever server the chain before it settled on. + QString previousServerHost; + QString previousServerPort; + + // Recorded at parse time when the user confirmed migrating away from a live + // session to the host/port named by the link. + QString migrationTargetHost; + QString migrationTargetPort; + bool pendingRestore = false; + bool succeeded = false; +}; + /** * @brief Parses cockatrice:// links and runs them as serialized intent chains. * @@ -30,28 +54,18 @@ signals: void urlChainFinished(bool connected); private: - Intent *createJoinGameIntent(const QUrlQuery &query, QList &chain); - Intent *createOpenDeckIntent(const QUrlQuery &query, QList &chain); + Intent *createJoinGameIntent(const QUrlQuery &query, PendingIntentChain &chain); + Intent *createOpenDeckIntent(const QUrlQuery &query, PendingIntentChain &chain); QString generateJoinGameMessage(const ContextJoinGame &context, const QString &gameDescription); [[nodiscard]] bool isConnectedTo(const QString &hostname, const QString &port) const; void startNextChain(); - void chainEnded(); - void restorePreviousServer(); - void restoreToPreviousServer(); + void chainEnded(bool chainSucceeded); + void restorePreviousServer(const PendingIntentChain &chain); + void restoreToPreviousServer(const PendingIntentChain &chain); MainWindow *mainWindow; - QList> pendingChains; + QList pendingChains; bool chainRunning = false; - bool currentChainSucceeded = false; - - // Set when an open-deck link migrates the session to another server. If the - // chain then fails or is cancelled while still on that server, the previous - // session is restored (reconnect if credentials are saved, else disconnect). - QString migrationTargetHost; - QString migrationTargetPort; - QString previousServerHost; - QString previousServerPort; - bool pendingRestore = false; }; #endif // COCKATRICE_URL_PARSER_H