mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[DeckShare] Open shared decks via links with a gated preview flow (#7244)
* [DeckShare] Open shared decks via links with a gated preview flow - Serialized url-chain dispatcher in IntentUrlParser; queue-drained urlChainFinished(bool) drives the startup auto-connect fallback - Open-shared-deck intent with sequential download state machine, 15s per-item timeout, partial-success offer, livable Cancel via ApplicationModal dlg_login_prompt interactive fallback - Preview dialog: download progress label, share vocab sweep, palette-highlight selection frame, Space/Enter keyboard toggle, NoFocus checkbox, double-click tile opens immediately - Confirm-before-server-migration with one-shot restore to the previous server on failed/cancelled chains (statusChanged settle deferral), hostname-only identity comparisons - Skip credential link when already connected; arrow-key navigation in FlowWidget; card glows use palette highlight - Address code-review M1-M4 and UI/UX QA blockers 1-2 * [DeckShare] End the open-shared-deck files with a trailing newline * [DeckShare] Forward a dependency's cancellation as the owner's own * [DeckShare] Let intent chains opt into the link sign-in dialog * [DeckShare] Track link-intent chains per-run so each can restore its own session * [Settings] Match a server on the exact host and port when adding it * [DeckShare] Confirm the share link's target server before opening a deck * [DeckShare] Reformat the link sign-in intent constructor * [DeckShare] Time the share-list round trip and backstop silently-destroyed intent chains * [Client] Drain a single-instance payload before its handlers read the socket again * [Client] Treat a busy single-instance primary as alive instead of stealing its socket * [DeckShare] Keep arrow-key navigation between flow items inside a scroll area * [Client] Skip the startup connection when a macOS URL launch owns the connection * [Client] Redact share secrets from activation URL logs * [Client] Make the link-connection gates port-aware and keyboard-safe Second-pass review notes for the shared-deck link flow (Cockatrice#7244): - FlowWidget arrow-key navigation is opt-in via addNavigableWidget, so combo/spin controls on the analytics flows keep their own arrow keys - isConnectedTo and the open-deck/join-game preconditions compare the configured server port alongside the host, so a same-host/different-port link cannot resolve its share token or game id on the wrong instance - the link sign-in dialog reuses an existing server entry's saved name instead of renaming it to the raw hostname - skipStartupAutoConnect is cleared once the launch chain connects, so a later mid-session declined link cannot fire the startup fallback - the plain-launch path of SingleInstanceManager no longer blocks on the primary's ACK - link- and server-supplied text is html-escaped in the confirm prompts and shared-deck preview so markup cannot spoof the shown messages --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
ba2900dcb9
commit
8ca749c07d
29 changed files with 1666 additions and 86 deletions
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
#include <QDir>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Sent by the primary instance after it has read a forwarded payload. Without
|
||||
// an acknowledgment, a second instance cannot tell a live primary apart from a
|
||||
// stale socket left behind by a process that is still shutting down.
|
||||
const QByteArray ACK_MESSAGE = QByteArrayLiteral("COCKATRICE_ACK");
|
||||
} // namespace
|
||||
|
||||
SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
|
@ -20,9 +28,15 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend)
|
|||
}
|
||||
serverName = QStringLiteral("CockatriceSingleInstance-%1").arg(userName);
|
||||
|
||||
// Hand off to an already-running primary instance if one exists.
|
||||
if (forwardToPrimary(filesToSend)) {
|
||||
return false;
|
||||
// Hand off to an already-running primary instance if one exists. Never steal
|
||||
// the socket of a busy primary: it is alive and will act on the payload.
|
||||
switch (forwardToPrimary(filesToSend)) {
|
||||
case ForwardResult::Delivered:
|
||||
return false;
|
||||
case ForwardResult::PrimaryBusy:
|
||||
return false;
|
||||
case ForwardResult::NoPrimary:
|
||||
break;
|
||||
}
|
||||
|
||||
// No primary instance is currently reachable, so become the primary.
|
||||
|
|
@ -35,12 +49,18 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend)
|
|||
|
||||
// Another instance may have started while we were probing; hand off to it
|
||||
// instead of stealing its socket.
|
||||
if (forwardToPrimary(filesToSend)) {
|
||||
return false;
|
||||
switch (forwardToPrimary(filesToSend)) {
|
||||
case ForwardResult::Delivered:
|
||||
return false;
|
||||
case ForwardResult::PrimaryBusy:
|
||||
return false;
|
||||
case ForwardResult::NoPrimary:
|
||||
break;
|
||||
}
|
||||
|
||||
// The socket is stale (left over by a crashed instance): remove it and
|
||||
// retry. If that still fails, another instance just took the name.
|
||||
// The socket is stale (left over by a crashed instance), so no primary is
|
||||
// holding it: remove it and retry. If that still fails, another instance
|
||||
// just took the name.
|
||||
QLocalServer::removeServer(serverName);
|
||||
if (server->listen(serverName)) {
|
||||
return true;
|
||||
|
|
@ -50,12 +70,12 @@ bool SingleInstanceManager::tryRun(const QStringList &filesToSend)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend)
|
||||
SingleInstanceManager::ForwardResult SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend)
|
||||
{
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(serverName);
|
||||
if (!socket.waitForConnected(200)) {
|
||||
return false;
|
||||
return ForwardResult::NoPrimary;
|
||||
}
|
||||
|
||||
// Serialize payload with length prefix
|
||||
|
|
@ -72,7 +92,23 @@ bool SingleInstanceManager::forwardToPrimary(const QStringList &filesToSend)
|
|||
socket.flush();
|
||||
socket.waitForBytesWritten(1000);
|
||||
|
||||
return true;
|
||||
// A plain launch has nothing for the primary to act on, so there is nothing
|
||||
// to acknowledge. Waiting here would block the new instance for seconds if
|
||||
// the primary is busy in a modal dialog, so only the activation path (which
|
||||
// needs the ACK to avoid stealing a live primary's socket) waits below.
|
||||
if (filesToSend.isEmpty()) {
|
||||
return ForwardResult::Delivered;
|
||||
}
|
||||
|
||||
// Only report a successful hand-off once the primary has acknowledged that
|
||||
// it actually read the payload. A socket that connects but is still working
|
||||
// on an earlier payload is alive but busy, not dead: give it more room
|
||||
// before giving up, so a slow handler does not make a live primary look
|
||||
// dead (which would lead to stealing its socket).
|
||||
if (!socket.waitForReadyRead(1000) && !socket.waitForReadyRead(4000)) {
|
||||
return ForwardResult::PrimaryBusy;
|
||||
}
|
||||
return socket.readAll() == ACK_MESSAGE ? ForwardResult::Delivered : ForwardResult::PrimaryBusy;
|
||||
}
|
||||
|
||||
void SingleInstanceManager::handleNewConnection()
|
||||
|
|
@ -111,12 +147,23 @@ void SingleInstanceManager::handleNewConnection()
|
|||
QStringList files;
|
||||
payloadStream >> files;
|
||||
|
||||
emit filesReceived(files);
|
||||
// Acknowledge receipt as soon as the payload is parsed, before the
|
||||
// primary starts handling it. The handlers run synchronously and can
|
||||
// take longer than the sender's readiness timeout (e.g. a modal
|
||||
// confirmation box), which would otherwise make a live primary look
|
||||
// dead and cause duplicate handling.
|
||||
socket->write(ACK_MESSAGE);
|
||||
socket->flush();
|
||||
|
||||
// Reset buffer (single message use-case)
|
||||
// Drop the payload from the buffer before handling it: the handlers
|
||||
// run synchronously and can spin a nested event loop (e.g. a modal
|
||||
// dialog) that re-reads this socket, which would re-parse and re-emit
|
||||
// the same files.
|
||||
buffer->clear();
|
||||
*expectedSize = 0;
|
||||
|
||||
emit filesReceived(files);
|
||||
|
||||
socket->disconnectFromServer();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue