From 0d7196fd81d15f3f9ae80027531d0e87b4af4152 Mon Sep 17 00:00:00 2001 From: askmeaboutloom Date: Fri, 30 Aug 2024 14:50:48 +0200 Subject: [PATCH] Implement streamed session autoresets Redesigns the way autoresets work a bunch. The server now sends a unique payload along with the autoreset request message. This is used by clients to recognize that the server supports this new style of autoreset and to correlate autoresets in the unlikely case of them overtaking each other. The autoreset now doesn't immediately start upon the first client responding. Instead, clients send along information about their operating system, recent latency measurements, reset capabilities (currently only the new "stream" capability) and if the user marked their network connection as "poor". The server collects these (under a time limit) and then determines the best client to perform the autoreset. At the time of writing, this goes in the order of: * Supporting streamed resets * Better operating systems (Android or the browser are worse) * Better network quality (according to client settings) * Lower average ping * Earlier autoreset response Whoever client gets the cut gets to perform the autoreset. If they don't support streamed autoresets, it proceeds as before. If they do support streamed autoresets, they are instructed to perform one of those. The client will send a stream-reset-start message, upon which the server will enqueue a soft reset and a start marker for the client, using the autoreset payload as the correlator. The client in turn uses this message with the matching correlator to send themselves an internal message through the paint engine that will give them a clean canvas state to build a reset image from. The reset image is packed into a new ResetStream meta message. This message contains gzip-compressed messages for the new reset image, which the server will decompress into a new, parallel image. Each of these message is acknowledged by the server before the client sends the next one, as to not their upstream entirely. Once the client is done sending their reset image, they tell the server that they're done and, as a sanity check, how many messages they expect to have been decompressed. The reset stream is then put into a pending state, to be resolved once all clients are caught up to the messages that have been added since after the reset stream was started (which, in most cases, will already be the case.) Once that occurs, the reset is resolved: all messages that have been added since the reset stream was started are packed on top of that new reset image and the session history is replaced with this new state. History positions of clients are shifted forward into the new area, block caches are shifted over to the new offsets etc. Server-side recordings (the ones done by the --record flag, not the .archived ones) are not touched, they will continue recording. The resulting state *should* be identical to what clients are already seeing, so they receive no indication that the session has been reset other than the session size in the corner shrinking to a smaller value. The client that does the streaming will get a message in the bottom-right corner telling them that they're compressing the session and uploading it, to give them an indication as to why Drawpile is using more resources or their network is more saturated. When an autoreset fails, the request state is reset and another round will be attempted after a delay. This also makes some ancillary changes along the way, like sending session size updates at a more reasonable time after a reset, replacing the int and uint for history positions and sizes with long long and size_t, as well as checking journal writes for failures. Setting the network quality to "poor" also no longer results in autoresets to be disabled entirely, instead they either transmit a bad network score under the new system or just delay the autoreset response by a bunch unter the old one. This relates to #1247. --- .../dialogs/settingsdialog/network.cpp | 7 - src/desktop/mainwindow.cpp | 18 +- src/desktop/mainwindow.h | 1 + src/desktop/scene/canvasscene.cpp | 46 ++ src/desktop/scene/canvasscene.h | 4 + src/desktop/scene/noticeitem.cpp | 2 +- src/desktop/scene/noticeitem.h | 1 + src/desktop/scene/scenewrapper.cpp | 7 + src/desktop/scene/scenewrapper.h | 1 + src/desktop/view/canvasscene.cpp | 49 ++ src/desktop/view/canvasscene.h | 4 + src/desktop/view/canvaswrapper.h | 1 + src/desktop/view/viewwrapper.cpp | 5 + src/desktop/view/viewwrapper.h | 1 + .../generators/protogen/protocol.yaml | 8 + .../libengine/dpengine/canvas_history.c | 19 + .../libengine/dpengine/canvas_history.h | 3 + .../libengine/dpengine/paint_engine.c | 28 +- .../libengine/dpengine/paint_engine.h | 8 +- src/drawdance/libmsg/CMakeLists.txt | 2 + src/drawdance/libmsg/dpmsg/message_queue.c | 38 + src/drawdance/libmsg/dpmsg/message_queue.h | 12 + src/drawdance/libmsg/dpmsg/messages.c | 124 +++ src/drawdance/libmsg/dpmsg/messages.h | 35 + src/drawdance/libmsg/dpmsg/msg_internal.c | 37 + src/drawdance/libmsg/dpmsg/msg_internal.h | 8 + src/drawdance/libmsg/dpmsg/reset_stream.c | 350 ++++++++ src/drawdance/libmsg/dpmsg/reset_stream.h | 41 + .../libmsg/test/read_write_roundtrip.c | 231 ++++-- src/drawdance/rust/bindings.rs | 64 ++ src/drawdance/rust/engine/paint_engine.rs | 2 + src/libclient/canvas/canvasmodel.cpp | 6 +- src/libclient/canvas/canvasmodel.h | 3 +- src/libclient/canvas/paintengine.cpp | 18 +- src/libclient/canvas/paintengine.h | 5 + src/libclient/document.cpp | 336 +++++++- src/libclient/document.h | 40 +- src/libclient/drawdance/paintengine.cpp | 8 +- src/libclient/drawdance/paintengine.h | 4 + src/libclient/net/client.cpp | 21 +- src/libclient/net/client.h | 6 +- src/libclient/net/message.cpp | 13 +- src/libclient/net/message.h | 5 +- src/libclient/server/builtinsession.cpp | 58 +- src/libclient/server/builtinsession.h | 19 +- src/libserver/client.cpp | 18 +- src/libserver/client.h | 22 +- src/libserver/filedhistory.cpp | 683 +++++++++++----- src/libserver/filedhistory.h | 100 ++- src/libserver/inmemoryhistory.cpp | 68 +- src/libserver/inmemoryhistory.h | 29 +- src/libserver/loginhandler.cpp | 1 + src/libserver/opcommands.cpp | 134 +++- src/libserver/session.cpp | 49 +- src/libserver/session.h | 39 +- src/libserver/sessionhistory.cpp | 227 +++++- src/libserver/sessionhistory.h | 180 ++++- src/libserver/thinserverclient.cpp | 32 +- src/libserver/thinserverclient.h | 8 +- src/libserver/thinsession.cpp | 754 ++++++++++++++++-- src/libserver/thinsession.h | 59 +- src/libshared/net/message.cpp | 17 + src/libshared/net/message.h | 4 + src/libshared/net/servercmd.cpp | 81 +- src/libshared/net/servercmd.h | 17 +- 65 files changed, 3742 insertions(+), 479 deletions(-) create mode 100644 src/drawdance/libmsg/dpmsg/reset_stream.c create mode 100644 src/drawdance/libmsg/dpmsg/reset_stream.h diff --git a/src/desktop/dialogs/settingsdialog/network.cpp b/src/desktop/dialogs/settingsdialog/network.cpp index 849adc42cd..af1ea4c722 100644 --- a/src/desktop/dialogs/settingsdialog/network.cpp +++ b/src/desktop/dialogs/settingsdialog/network.cpp @@ -124,13 +124,6 @@ void Network::initNetwork( form, tr("Connection quality:"), true, {{tr("Good"), 1}, {tr("Poor"), 0}}); settings.bindServerAutoReset(autoReset); - auto *autoResetNote = utils::formNote( - tr("If all operators in a session set connection quality to Poor, " - "auto-reset will not work and the server will stop processing " - "updates until the session is manually reset."), - QSizePolicy::Label, QIcon::fromTheme("dialog-warning")); - form->addRow(nullptr, autoResetNote); - settings.bindServerAutoReset(autoResetNote, &QWidget::setHidden); auto *timeout = new QSpinBox(this); timeout->setAlignment(Qt::AlignLeft); diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp index 225ca1a80c..cae8ad91f1 100644 --- a/src/desktop/mainwindow.cpp +++ b/src/desktop/mainwindow.cpp @@ -373,8 +373,15 @@ MainWindow::MainWindow(bool restoreWindowPosition, bool singleSession) m_dockToolSettings, &docks::ToolSettings::resetColors); // Network client <-> UI connections - connect(m_doc, &Document::catchupProgress, this, &MainWindow::updateCatchupProgress); - connect(m_doc, &Document::catchupProgress, m_netstatus, &widgets::NetStatus::setCatchupProgress); + connect( + m_doc, &Document::catchupProgress, this, + &MainWindow::updateCatchupProgress); + connect( + m_doc, &Document::catchupProgress, m_netstatus, + &widgets::NetStatus::setCatchupProgress); + connect( + m_doc, &Document::streamResetProgress, this, + &MainWindow::updateStreamResetProgress); connect( m_doc->client(), &net::Client::serverStatusUpdate, m_viewStatusBar, @@ -1855,6 +1862,11 @@ void MainWindow::updateCatchupProgress(int percent) m_canvasView->setCatchupProgress(percent, false); } +void MainWindow::updateStreamResetProgress(int percent) +{ + m_canvasView->setStreamResetProgress(percent); +} + void MainWindow::savePreResetImageAs() { if(m_preResetCanvasState.isNull()) { @@ -2392,7 +2404,7 @@ void MainWindow::leave() } }); - if(m_doc->client()->uploadQueueBytes() > 0) { + if(m_doc->client()->uploadQueueBytes() > 0 || m_doc->isStreamingReset()) { leavebox->setIcon(QMessageBox::Warning); leavebox->setInformativeText(tr("There is still unsent data! Please wait until transmission completes!")); } diff --git a/src/desktop/mainwindow.h b/src/desktop/mainwindow.h index b5b22d8a88..50f656795d 100644 --- a/src/desktop/mainwindow.h +++ b/src/desktop/mainwindow.h @@ -194,6 +194,7 @@ private slots: void showResetNoticeDialog(const drawdance::CanvasState &canvasState); void updateCatchupProgress(int percent); + void updateStreamResetProgress(int percent); void showCompatibilityModeWarning(); void onOperatorModeChange(bool op); diff --git a/src/desktop/scene/canvasscene.cpp b/src/desktop/scene/canvasscene.cpp index cfcbce06a4..5568a8d497 100644 --- a/src/desktop/scene/canvasscene.cpp +++ b/src/desktop/scene/canvasscene.cpp @@ -12,6 +12,7 @@ #include "desktop/scene/transformitem.h" #include "desktop/scene/usermarkeritem.h" #include "desktop/utils/widgetutils.h" +#include "desktop/view/canvasscene.h" #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/layerlist.h" #include "libclient/canvas/paintengine.h" @@ -41,6 +42,7 @@ CanvasScene::CanvasScene(QObject *parent) , m_lockNotice(nullptr) , m_toolNotice(nullptr) , m_catchup(nullptr) + , m_streamResetNotice(nullptr) , m_showAnnotationBorders(false) , m_showAnnotations(true) , m_showUserMarkers(true) @@ -188,6 +190,9 @@ void CanvasScene::setSceneBounds(const QRectF &sceneBounds) if(m_catchup) { setCatchupPosition(); } + if(m_streamResetNotice) { + setStreamResetNoticePosition(); + } for(ToggleItem *ti : m_toggleItems) { ti->updateSceneBounds(sceneBounds); } @@ -306,6 +311,30 @@ void CanvasScene::setCatchupProgress(int percent) setCatchupPosition(); } +void CanvasScene::setStreamResetProgress(int percent) +{ + if(percent > 100) { + if(m_streamResetNotice) { + m_streamResetNotice->setText( + view::CanvasScene::getStreamResetProgressText(percent)); + if(m_streamResetNotice->persist() < 0.0) { + m_streamResetNotice->setPersist(NOTICE_PERSIST); + } + } + } else { + if(m_streamResetNotice) { + m_streamResetNotice->setText( + view::CanvasScene::getStreamResetProgressText(percent)); + m_streamResetNotice->setPersist(-1.0); + } else { + m_streamResetNotice = new NoticeItem( + view::CanvasScene::getStreamResetProgressText(percent)); + addItem(m_streamResetNotice); + } + setStreamResetNoticePosition(); + } +} + void CanvasScene::onUserJoined(int id, const QString &name) { Q_UNUSED(name); @@ -534,6 +563,11 @@ void CanvasScene::advanceAnimations() delete m_catchup; m_catchup = nullptr; } + + if(m_streamResetNotice && !m_streamResetNotice->animationStep(STEP)) { + delete m_streamResetNotice; + m_streamResetNotice = nullptr; + } } void CanvasScene::laserTrail(int userId, int persistence, const QColor &color) @@ -743,6 +777,18 @@ void CanvasScene::setCatchupPosition() catchupBounds.height() + NOTICE_OFFSET)); } +void CanvasScene::setStreamResetNoticePosition() +{ + qreal catchupOffset = + m_catchup ? m_catchup->boundingRect().height() + NOTICE_OFFSET : 0.0; + QRectF streamResetNoticeBounds = m_streamResetNotice->boundingRect(); + m_streamResetNotice->updatePosition( + sceneRect().bottomRight() - + QPointF( + streamResetNoticeBounds.width() + NOTICE_OFFSET, + streamResetNoticeBounds.height() + NOTICE_OFFSET + catchupOffset)); +} + void CanvasScene::setShowOwnUserMarker(bool showOwnUserMarker) { if(showOwnUserMarker != m_showOwnUserMarker) { diff --git a/src/desktop/scene/canvasscene.h b/src/desktop/scene/canvasscene.h index 82e6ce1dc3..895f8dd1e5 100644 --- a/src/desktop/scene/canvasscene.h +++ b/src/desktop/scene/canvasscene.h @@ -147,6 +147,8 @@ public slots: void setCatchupProgress(int percent); + void setStreamResetProgress(int percent); + signals: //! Canvas size has just changed void canvasResized(int xoffset, int yoffset, const QSize &oldSize); @@ -180,6 +182,7 @@ private slots: void setLockNoticePosition(); void setToolNoticePosition(bool initial); void setCatchupPosition(); + void setStreamResetNoticePosition(); //! The actual canvas model canvas::CanvasModel *m_model; @@ -206,6 +209,7 @@ private slots: NoticeItem *m_lockNotice; NoticeItem *m_toolNotice; CatchupItem *m_catchup; + NoticeItem *m_streamResetNotice; QVector m_toggleItems; OutlineItem *m_outlineItem; diff --git a/src/desktop/scene/noticeitem.cpp b/src/desktop/scene/noticeitem.cpp index d060c7f3d9..f5665c012b 100644 --- a/src/desktop/scene/noticeitem.cpp +++ b/src/desktop/scene/noticeitem.cpp @@ -41,7 +41,7 @@ bool NoticeItem::setPersist(qreal seconds) bool changed = seconds != m_persist && (seconds < FADEOUT || m_persist < FADEOUT); m_persist = seconds; - setOpacity(qBound(0.0, m_persist / FADEOUT, 1.0)); + setOpacity(m_persist < 0.0 ? 1.0 : qBound(0.0, m_persist / FADEOUT, 1.0)); if(changed) { refresh(); } diff --git a/src/desktop/scene/noticeitem.h b/src/desktop/scene/noticeitem.h index 9dd97de4fd..2e40be1134 100644 --- a/src/desktop/scene/noticeitem.h +++ b/src/desktop/scene/noticeitem.h @@ -18,6 +18,7 @@ class NoticeItem final : public BaseItem { bool setText(const QString &text); + qreal persist() const { return m_persist; } bool setPersist(qreal seconds); bool setOpacity(qreal opacity); diff --git a/src/desktop/scene/scenewrapper.cpp b/src/desktop/scene/scenewrapper.cpp index 76cf605a87..9d4cbe4d10 100644 --- a/src/desktop/scene/scenewrapper.cpp +++ b/src/desktop/scene/scenewrapper.cpp @@ -158,6 +158,13 @@ void SceneWrapper::setCatchupProgress(int percent, bool force) } } +void SceneWrapper::setStreamResetProgress(int percent) +{ + if(m_scene) { + m_scene->setStreamResetProgress(percent); + } +} + void SceneWrapper::setSaveInProgress(bool saveInProgress) { m_view->setSaveInProgress(saveInProgress); diff --git a/src/desktop/scene/scenewrapper.h b/src/desktop/scene/scenewrapper.h index 177cc61c9c..43dcdee64b 100644 --- a/src/desktop/scene/scenewrapper.h +++ b/src/desktop/scene/scenewrapper.h @@ -50,6 +50,7 @@ class SceneWrapper final : public QObject, public view::CanvasWrapper { void setShowSelectionMask(bool showSelectionMask) override; void setCatchupProgress(int percent, bool force) override; + void setStreamResetProgress(int percent) override; void setSaveInProgress(bool saveInProgress) override; void showDisconnectedWarning( diff --git a/src/desktop/view/canvasscene.cpp b/src/desktop/view/canvasscene.cpp index 1915a960cc..7e33a5a0bf 100644 --- a/src/desktop/view/canvasscene.cpp +++ b/src/desktop/view/canvasscene.cpp @@ -497,6 +497,35 @@ void CanvasScene::setCatchupProgress(int percent) setCatchupPosition(); } +void CanvasScene::setStreamResetProgress(int percent) +{ + if(percent > 100) { + if(m_streamResetNotice) { + m_streamResetNotice->setText(getStreamResetProgressText(percent)); + if(m_streamResetNotice->persist() < 0.0) { + m_streamResetNotice->setPersist(NOTICE_PERSIST); + } + } + } else { + if(m_streamResetNotice) { + m_streamResetNotice->setText(getStreamResetProgressText(percent)); + m_streamResetNotice->setPersist(-1.0); + } else { + m_streamResetNotice = + new NoticeItem(getStreamResetProgressText(percent)); + addSceneItem(m_streamResetNotice); + } + setStreamResetNoticePosition(); + } +} + +QString CanvasScene::getStreamResetProgressText(int percent) +{ + return percent < 0 + ? tr("Compressing canvas…") + : tr("Uploading canvas %1%").arg(qBound(0, percent, 100)); +} + int CanvasScene::checkHover(const QPointF &scenePos, bool *outWasHovering) { ToggleItem::Action action = ToggleItem::Action::None; @@ -539,6 +568,9 @@ void CanvasScene::onSceneRectChanged() if(m_catchup) { setCatchupPosition(); } + if(m_streamResetNotice) { + setStreamResetNoticePosition(); + } setTogglePositions(); } @@ -773,6 +805,18 @@ void CanvasScene::setCatchupPosition() catchupBounds.height() + NOTICE_OFFSET)); } +void CanvasScene::setStreamResetNoticePosition() +{ + qreal catchupOffset = + m_catchup ? m_catchup->boundingRect().height() + NOTICE_OFFSET : 0.0; + QRectF streamResetNoticeBounds = m_streamResetNotice->boundingRect(); + m_streamResetNotice->updatePosition( + sceneRect().bottomRight() - + QPointF( + streamResetNoticeBounds.width() + NOTICE_OFFSET, + streamResetNoticeBounds.height() + NOTICE_OFFSET + catchupOffset)); +} + void CanvasScene::setTogglePositions() { for(ToggleItem *ti : m_toggleItems) { @@ -814,6 +858,11 @@ void CanvasScene::advanceAnimations() m_catchup = nullptr; } + if(m_streamResetNotice && !m_streamResetNotice->animationStep(dt)) { + delete m_streamResetNotice; + m_streamResetNotice = nullptr; + } + m_animationElapsedTimer.restart(); } diff --git a/src/desktop/view/canvasscene.h b/src/desktop/view/canvasscene.h index 49b3676dc2..77b8ffe771 100644 --- a/src/desktop/view/canvasscene.h +++ b/src/desktop/view/canvasscene.h @@ -109,6 +109,8 @@ class CanvasScene final : public QGraphicsScene { bool hasCatchup() const; void setCatchupProgress(int percent); + void setStreamResetProgress(int percent); + static QString getStreamResetProgressText(int percent); int checkHover(const QPointF &scenePos, bool *outWasHovering = nullptr); void removeHover(); @@ -140,6 +142,7 @@ class CanvasScene final : public QGraphicsScene { void setLockNoticePosition(); void setToolNoticePosition(bool initial); void setCatchupPosition(); + void setStreamResetNoticePosition(); void setTogglePositions(); void advanceAnimations(); @@ -183,6 +186,7 @@ class CanvasScene final : public QGraphicsScene { NoticeItem *m_toolNotice = nullptr; CatchupItem *m_catchup = nullptr; + NoticeItem *m_streamResetNotice = nullptr; QVector m_toggleItems; diff --git a/src/desktop/view/canvaswrapper.h b/src/desktop/view/canvaswrapper.h index 7446e93e74..09aded1e25 100644 --- a/src/desktop/view/canvaswrapper.h +++ b/src/desktop/view/canvaswrapper.h @@ -111,6 +111,7 @@ class CanvasWrapper { virtual void setShowSelectionMask(bool showSelectionMask) = 0; virtual void setCatchupProgress(int percent, bool force) = 0; + virtual void setStreamResetProgress(int percent) = 0; virtual void setSaveInProgress(bool saveInProgress) = 0; virtual void diff --git a/src/desktop/view/viewwrapper.cpp b/src/desktop/view/viewwrapper.cpp index 94cd24057f..097fd12cc8 100644 --- a/src/desktop/view/viewwrapper.cpp +++ b/src/desktop/view/viewwrapper.cpp @@ -147,6 +147,11 @@ void ViewWrapper::setCatchupProgress(int percent, bool force) } } +void ViewWrapper::setStreamResetProgress(int percent) +{ + m_scene->setStreamResetProgress(percent); +} + void ViewWrapper::setSaveInProgress(bool saveInProgress) { m_controller->setSaveInProgress(saveInProgress); diff --git a/src/desktop/view/viewwrapper.h b/src/desktop/view/viewwrapper.h index 527185d200..3acd40668a 100644 --- a/src/desktop/view/viewwrapper.h +++ b/src/desktop/view/viewwrapper.h @@ -47,6 +47,7 @@ class ViewWrapper final : public QObject, public CanvasWrapper { void setShowSelectionMask(bool showSelectionMask) override; void setCatchupProgress(int percent, bool force) override; + void setStreamResetProgress(int percent) override; void setSaveInProgress(bool saveInProgress) override; void showDisconnectedWarning( diff --git a/src/drawdance/generators/protogen/protocol.yaml b/src/drawdance/generators/protogen/protocol.yaml index c0a8aa623f..0b88e804d7 100644 --- a/src/drawdance/generators/protogen/protocol.yaml +++ b/src/drawdance/generators/protogen/protocol.yaml @@ -168,6 +168,14 @@ PrivateChat: - oflags u8 - message utf8 +ResetStream: + id: 39 + comment: | + Streamed chunk of session reset messages. The client and server + will negotiate support and compression algorithm. + fields: + - data bytes + # Meta messages (opaque) diff --git a/src/drawdance/libengine/dpengine/canvas_history.c b/src/drawdance/libengine/dpengine/canvas_history.c index eefd469875..a4983753e1 100644 --- a/src/drawdance/libengine/dpengine/canvas_history.c +++ b/src/drawdance/libengine/dpengine/canvas_history.c @@ -896,6 +896,25 @@ void DP_canvas_history_soft_reset(DP_CanvasHistory *ch, DP_DrawContext *dc, } } +DP_CanvasState *DP_canvas_history_stream_start_state_inc(DP_CanvasHistory *ch, + DP_DrawContext *dc) +{ + DP_ASSERT(ch); + bool have_fork = have_local_fork(ch); + if (have_fork) { + search_and_replay_from(ch, dc, ch->fork.start - ch->offset, false); + } + + DP_CanvasState *cs = DP_canvas_state_incref(ch->current_state); + + if (have_fork) { + finish_replay(ch, replay_fork_dec(ch, DP_canvas_state_incref(cs), dc), + dc); + } + + return cs; +} + int DP_canvas_history_undo_depth_limit(DP_CanvasHistory *ch) { DP_ASSERT(ch); diff --git a/src/drawdance/libengine/dpengine/canvas_history.h b/src/drawdance/libengine/dpengine/canvas_history.h index cf22cefdeb..26b70e3a54 100644 --- a/src/drawdance/libengine/dpengine/canvas_history.h +++ b/src/drawdance/libengine/dpengine/canvas_history.h @@ -113,6 +113,9 @@ void DP_canvas_history_soft_reset(DP_CanvasHistory *ch, DP_DrawContext *dc, unsigned int context_id, DP_CanvasHistorySoftResetFn fn, void *user); +DP_CanvasState *DP_canvas_history_stream_start_state_inc(DP_CanvasHistory *ch, + DP_DrawContext *dc); + int DP_canvas_history_undo_depth_limit(DP_CanvasHistory *ch); void DP_canvas_history_undo_depth_limit_set(DP_CanvasHistory *ch, diff --git a/src/drawdance/libengine/dpengine/paint_engine.c b/src/drawdance/libengine/dpengine/paint_engine.c index 935ee769f0..76ff0e39e5 100644 --- a/src/drawdance/libengine/dpengine/paint_engine.c +++ b/src/drawdance/libengine/dpengine/paint_engine.c @@ -153,6 +153,10 @@ struct DP_PaintEngine { void *get_time_ms_user; } record; DP_PaintEnginePlayback playback; + struct { + DP_PaintEngineStreamResetStartFn start_fn; + void *user; + } stream_reset; }; @@ -339,6 +343,24 @@ static void handle_internal(DP_PaintEngine *pe, DP_DrawContext *dc, DP_warn("Error clearing local fork: %s", DP_error()); } break; + case DP_MSG_INTERNAL_TYPE_STREAM_RESET_START: { + DP_PaintEngineStreamResetStartFn stream_reset_start_fn = + pe->stream_reset.start_fn; + if (stream_reset_start_fn) { + size_t correlator_length; + const char *correlator = + DP_msg_internal_stream_reset_start_correlator( + mi, &correlator_length); + stream_reset_start_fn( + pe->stream_reset.user, + DP_canvas_history_stream_start_state_inc(pe->ch, dc), + correlator_length, correlator); + } + else { + DP_warn("No stream reset start callback set"); + } + break; + } default: DP_warn("Unhandled internal message type %d", (int)type); break; @@ -663,7 +685,9 @@ DP_PaintEngine *DP_paint_engine_new_inc( bool want_canvas_history_dump, const char *canvas_history_dump_dir, DP_RecorderGetTimeMsFn get_time_ms_fn, void *get_time_ms_user, DP_Player *player_or_null, DP_PaintEnginePlaybackFn playback_fn, - DP_PaintEngineDumpPlaybackFn dump_playback_fn, void *playback_user) + DP_PaintEngineDumpPlaybackFn dump_playback_fn, void *playback_user, + DP_PaintEngineStreamResetStartFn stream_reset_start_fn, + void *stream_reset_user) { DP_PaintEngine *pe = DP_malloc(sizeof(*pe)); @@ -731,6 +755,8 @@ DP_PaintEngine *DP_paint_engine_new_inc( pe->playback.fn = playback_fn; pe->playback.dump_fn = dump_playback_fn; pe->playback.user = playback_user; + pe->stream_reset.start_fn = stream_reset_start_fn; + pe->stream_reset.user = stream_reset_user; return pe; } diff --git a/src/drawdance/libengine/dpengine/paint_engine.h b/src/drawdance/libengine/dpengine/paint_engine.h index aad6d7d670..728cc860ec 100644 --- a/src/drawdance/libengine/dpengine/paint_engine.h +++ b/src/drawdance/libengine/dpengine/paint_engine.h @@ -55,6 +55,10 @@ typedef struct DP_LayerContent DP_TransientLayerContent; typedef void (*DP_PaintEnginePlaybackFn)(void *user, long long position); typedef void (*DP_PaintEngineDumpPlaybackFn)(void *user, long long position, DP_CanvasHistorySnapshot *chs); +// Takes ownership of the canvas state and must decrement the ref when done. +typedef void (*DP_PaintEngineStreamResetStartFn)(void *user, DP_CanvasState *cs, + size_t correlator_length, + const char *correlator); typedef void (*DP_PaintEngineAclsChangedFn)(void *user, int acl_change_flags); typedef void (*DP_PaintEngineLaserTrailFn)(void *user, unsigned int context_id, int persistence, uint32_t color); @@ -106,7 +110,9 @@ DP_PaintEngine *DP_paint_engine_new_inc( bool want_canvas_history_dump, const char *canvas_history_dump_dir, DP_RecorderGetTimeMsFn get_time_ms_fn, void *get_time_ms_user, DP_Player *player_or_null, DP_PaintEnginePlaybackFn playback_fn, - DP_PaintEngineDumpPlaybackFn dump_playback_fn, void *playback_user); + DP_PaintEngineDumpPlaybackFn dump_playback_fn, void *playback_user, + DP_PaintEngineStreamResetStartFn stream_reset_start_fn, + void *stream_reset_user); void DP_paint_engine_free_join(DP_PaintEngine *pe); diff --git a/src/drawdance/libmsg/CMakeLists.txt b/src/drawdance/libmsg/CMakeLists.txt index bfa7444448..0f38cca7c8 100644 --- a/src/drawdance/libmsg/CMakeLists.txt +++ b/src/drawdance/libmsg/CMakeLists.txt @@ -12,6 +12,7 @@ dp_target_sources(dpmsg dpmsg/message_queue.c dpmsg/msg_internal.c dpmsg/protover.c + dpmsg/reset_stream.c dpmsg/text_reader.c dpmsg/text_writer.c dpmsg/acl.h @@ -25,6 +26,7 @@ dp_target_sources(dpmsg dpmsg/message_queue.h dpmsg/msg_internal.h dpmsg/protover.h + dpmsg/reset_stream.h dpmsg/text_reader.h dpmsg/text_writer.h ) diff --git a/src/drawdance/libmsg/dpmsg/message_queue.c b/src/drawdance/libmsg/dpmsg/message_queue.c index bfa7945a73..640c13c80d 100644 --- a/src/drawdance/libmsg/dpmsg/message_queue.c +++ b/src/drawdance/libmsg/dpmsg/message_queue.c @@ -23,6 +23,7 @@ #include "message.h" #include #include +#include #define ELEMENT_SIZE sizeof(DP_Message *) @@ -79,3 +80,40 @@ DP_Message *DP_message_queue_shift(DP_Queue *queue) return NULL; } } + + +void DP_message_vector_init(DP_Vector *vec, size_t initial_capacity) +{ + DP_ASSERT(vec); + DP_vector_init(vec, initial_capacity, ELEMENT_SIZE); +} + +void DP_message_vector_dispose(DP_Vector *vec) +{ + DP_ASSERT(vec); + DP_vector_clear_dispose(vec, ELEMENT_SIZE, dispose_message); +} + +DP_Message *DP_message_vector_push_noinc(DP_Vector *vec, DP_Message *msg) +{ + DP_ASSERT(vec); + DP_ASSERT(msg); + DP_Message **pp = DP_vector_push(vec, ELEMENT_SIZE); + *pp = msg; + return msg; +} + +DP_Message *DP_message_vector_push_inc(DP_Vector *vec, DP_Message *msg) +{ + DP_ASSERT(vec); + DP_ASSERT(msg); + return DP_message_vector_push_noinc(vec, DP_message_incref(msg)); +} + +DP_Message *DP_message_vector_at(DP_Vector *vec, size_t index) +{ + DP_ASSERT(vec); + DP_ASSERT(index < vec->used); + DP_Message **pp = DP_vector_at(vec, ELEMENT_SIZE, index); + return *pp; +} diff --git a/src/drawdance/libmsg/dpmsg/message_queue.h b/src/drawdance/libmsg/dpmsg/message_queue.h index 8ca95426c0..f7d3d2b197 100644 --- a/src/drawdance/libmsg/dpmsg/message_queue.h +++ b/src/drawdance/libmsg/dpmsg/message_queue.h @@ -25,6 +25,7 @@ typedef struct DP_Message DP_Message; typedef struct DP_Queue DP_Queue; +typedef struct DP_Vector DP_Vector; void DP_message_queue_init(DP_Queue *queue, size_t initial_capacity); @@ -40,4 +41,15 @@ DP_Message *DP_message_queue_peek(DP_Queue *queue); DP_Message *DP_message_queue_shift(DP_Queue *queue); +void DP_message_vector_init(DP_Vector *vec, size_t initial_capacity); + +void DP_message_vector_dispose(DP_Vector *vec); + +DP_Message *DP_message_vector_push_noinc(DP_Vector *vec, DP_Message *msg); + +DP_Message *DP_message_vector_push_inc(DP_Vector *vec, DP_Message *msg); + +DP_Message *DP_message_vector_at(DP_Vector *vec, size_t index); + + #endif diff --git a/src/drawdance/libmsg/dpmsg/messages.c b/src/drawdance/libmsg/dpmsg/messages.c index d765476c0b..dd5fd70ec1 100644 --- a/src/drawdance/libmsg/dpmsg/messages.c +++ b/src/drawdance/libmsg/dpmsg/messages.c @@ -53,6 +53,7 @@ bool DP_message_type_server_meta(DP_MessageType type) case DP_MSG_TRUSTED_USERS: case DP_MSG_SOFT_RESET: case DP_MSG_PRIVATE_CHAT: + case DP_MSG_RESET_STREAM: return true; default: return false; @@ -159,6 +160,8 @@ const char *DP_message_type_name(DP_MessageType type) return "softreset"; case DP_MSG_PRIVATE_CHAT: return "privatechat"; + case DP_MSG_RESET_STREAM: + return "resetstream"; case DP_MSG_INTERVAL: return "interval"; case DP_MSG_LASER_TRAIL: @@ -301,6 +304,8 @@ const char *DP_message_type_enum_name(DP_MessageType type) return "DP_MSG_SOFT_RESET"; case DP_MSG_PRIVATE_CHAT: return "DP_MSG_PRIVATE_CHAT"; + case DP_MSG_RESET_STREAM: + return "DP_MSG_RESET_STREAM"; case DP_MSG_INTERVAL: return "DP_MSG_INTERVAL"; case DP_MSG_LASER_TRAIL: @@ -457,6 +462,9 @@ DP_MessageType DP_message_type_from_name(const char *type_name, else if (DP_str_equal(type_name, "privatechat")) { return DP_MSG_PRIVATE_CHAT; } + else if (DP_str_equal(type_name, "resetstream")) { + return DP_MSG_RESET_STREAM; + } else if (DP_str_equal(type_name, "interval")) { return DP_MSG_INTERVAL; } @@ -664,6 +672,8 @@ DP_Message *DP_message_deserialize_body(int type, unsigned int context_id, return DP_msg_soft_reset_deserialize(context_id, buf, length); case DP_MSG_PRIVATE_CHAT: return DP_msg_private_chat_deserialize(context_id, buf, length); + case DP_MSG_RESET_STREAM: + return DP_msg_reset_stream_deserialize(context_id, buf, length); case DP_MSG_INTERVAL: return DP_msg_interval_deserialize(context_id, buf, length); case DP_MSG_LASER_TRAIL: @@ -832,6 +842,8 @@ DP_Message *DP_message_parse_body(DP_MessageType type, unsigned int context_id, return DP_msg_soft_reset_parse(context_id, reader); case DP_MSG_PRIVATE_CHAT: return DP_msg_private_chat_parse(context_id, reader); + case DP_MSG_RESET_STREAM: + return DP_msg_reset_stream_parse(context_id, reader); case DP_MSG_INTERVAL: return DP_msg_interval_parse(context_id, reader); case DP_MSG_LASER_TRAIL: @@ -2184,6 +2196,118 @@ size_t DP_msg_private_chat_message_len(const DP_MsgPrivateChat *mpc) } +/* DP_MSG_RESET_STREAM */ + +struct DP_MsgResetStream { + uint16_t data_size; + unsigned char data[]; +}; + +static size_t msg_reset_stream_payload_length(DP_Message *msg) +{ + DP_MsgResetStream *mrs = DP_message_internal(msg); + return mrs->data_size; +} + +static size_t msg_reset_stream_serialize_payload(DP_Message *msg, + unsigned char *data) +{ + DP_MsgResetStream *mrs = DP_message_internal(msg); + size_t written = 0; + written += write_bytes(mrs->data, mrs->data_size, data + written); + DP_ASSERT(written == msg_reset_stream_payload_length(msg)); + return written; +} + +static bool msg_reset_stream_write_payload_text(DP_Message *msg, + DP_TextWriter *writer) +{ + DP_MsgResetStream *mrs = DP_message_internal(msg); + return DP_text_writer_write_base64(writer, "data", mrs->data, + mrs->data_size); +} + +static bool msg_reset_stream_equals(DP_Message *DP_RESTRICT msg, + DP_Message *DP_RESTRICT other) +{ + DP_MsgResetStream *a = DP_message_internal(msg); + DP_MsgResetStream *b = DP_message_internal(other); + return a->data_size == b->data_size + && memcmp(a->data, b->data, DP_uint16_to_size(a->data_size)) == 0; +} + +static const DP_MessageMethods msg_reset_stream_methods = { + msg_reset_stream_payload_length, + msg_reset_stream_serialize_payload, + msg_reset_stream_write_payload_text, + msg_reset_stream_equals, +}; + +DP_Message *DP_msg_reset_stream_new(unsigned int context_id, + void (*set_data)(size_t, unsigned char *, + void *), + size_t data_size, void *data_user) +{ + DP_Message *msg = DP_message_new( + DP_MSG_RESET_STREAM, context_id, &msg_reset_stream_methods, + DP_FLEX_SIZEOF(DP_MsgResetStream, data, data_size)); + DP_MsgResetStream *mrs = DP_message_internal(msg); + mrs->data_size = DP_size_to_uint16(data_size); + if (set_data) { + set_data(mrs->data_size, mrs->data, data_user); + } + return msg; +} + +DP_Message *DP_msg_reset_stream_deserialize(unsigned int context_id, + const unsigned char *buffer, + size_t length) +{ + if (length > 65535) { + DP_error_set("Wrong length for resetstream message; " + "expected between 0 and 65535, got %zu", + length); + return NULL; + } + size_t read = 0; + size_t data_bytes = length - read; + uint16_t data_size = DP_size_to_uint16(data_bytes); + void *data_user = (void *)(buffer + read); + return DP_msg_reset_stream_new(context_id, read_bytes, data_size, + data_user); +} + +DP_Message *DP_msg_reset_stream_parse(unsigned int context_id, + DP_TextReader *reader) +{ + size_t data_size; + DP_TextReaderParseParams data_params = + DP_text_reader_get_base64_string(reader, "data", &data_size); + return DP_msg_reset_stream_new(context_id, DP_text_reader_parse_base64, + data_size, &data_params); +} + +DP_MsgResetStream *DP_msg_reset_stream_cast(DP_Message *msg) +{ + return DP_message_cast(msg, DP_MSG_RESET_STREAM); +} + +const unsigned char *DP_msg_reset_stream_data(const DP_MsgResetStream *mrs, + size_t *out_size) +{ + DP_ASSERT(mrs); + if (out_size) { + *out_size = mrs->data_size; + } + return mrs->data; +} + +size_t DP_msg_reset_stream_data_size(const DP_MsgResetStream *mrs) +{ + return mrs->data_size; +} + + /* DP_MSG_INTERVAL */ struct DP_MsgInterval { diff --git a/src/drawdance/libmsg/dpmsg/messages.h b/src/drawdance/libmsg/dpmsg/messages.h index 329b604317..85beee9dc1 100644 --- a/src/drawdance/libmsg/dpmsg/messages.h +++ b/src/drawdance/libmsg/dpmsg/messages.h @@ -54,6 +54,7 @@ typedef enum DP_MessageType { DP_MSG_TRUSTED_USERS = 36, DP_MSG_SOFT_RESET = 37, DP_MSG_PRIVATE_CHAT = 38, + DP_MSG_RESET_STREAM = 39, DP_MSG_INTERVAL = 64, DP_MSG_LASER_TRAIL = 65, DP_MSG_MOVE_POINTER = 66, @@ -567,6 +568,40 @@ const char *DP_msg_private_chat_message(const DP_MsgPrivateChat *mpc, size_t DP_msg_private_chat_message_len(const DP_MsgPrivateChat *mpc); +/* + * DP_MSG_RESET_STREAM + * + * Streamed chunk of session reset messages. The client and server + * will negotiate support and compression algorithm. + */ + +#define DP_MSG_RESET_STREAM_STATIC_LENGTH 0 + +#define DP_MSG_RESET_STREAM_DATA_MIN_SIZE 0 +#define DP_MSG_RESET_STREAM_DATA_MAX_SIZE 65535 + +typedef struct DP_MsgResetStream DP_MsgResetStream; + +DP_Message *DP_msg_reset_stream_new(unsigned int context_id, + void (*set_data)(size_t, unsigned char *, + void *), + size_t data_size, void *data_user); + +DP_Message *DP_msg_reset_stream_deserialize(unsigned int context_id, + const unsigned char *buffer, + size_t length); + +DP_Message *DP_msg_reset_stream_parse(unsigned int context_id, + DP_TextReader *reader); + +DP_MsgResetStream *DP_msg_reset_stream_cast(DP_Message *msg); + +const unsigned char *DP_msg_reset_stream_data(const DP_MsgResetStream *mrs, + size_t *out_size); + +size_t DP_msg_reset_stream_data_size(const DP_MsgResetStream *mrs); + + /* * DP_MSG_INTERVAL * diff --git a/src/drawdance/libmsg/dpmsg/msg_internal.c b/src/drawdance/libmsg/dpmsg/msg_internal.c index 9efadd9e70..348f64ccbc 100644 --- a/src/drawdance/libmsg/dpmsg/msg_internal.c +++ b/src/drawdance/libmsg/dpmsg/msg_internal.c @@ -67,6 +67,12 @@ typedef struct DP_MsgInternalDumpCommand { DP_Message *messages[]; } DP_MsgInternalDumpCommand; +typedef struct DP_MsgInternalStreamResetStart { + DP_MsgInternal parent; + size_t correlator_length; + char correlator[]; +} DP_MsgInternalStreamResetStart; + static size_t payload_length(DP_UNUSED DP_Message *msg) { DP_warn("DP_MsgInternal: payload_length called on internal message"); @@ -221,6 +227,24 @@ DP_Message *DP_msg_internal_flush_new(unsigned int context_id) sizeof(DP_MsgInternal)); } +DP_Message *DP_msg_internal_stream_reset_start_new(unsigned int context_id, + size_t correlator_length, + const char *correlator) +{ + DP_ASSERT(correlator || correlator_length == 0); + DP_Message *msg = + msg_internal_new(context_id, DP_MSG_INTERNAL_TYPE_STREAM_RESET_START, + DP_FLEX_SIZEOF(DP_MsgInternalStreamResetStart, + correlator, correlator_length + 1)); + DP_MsgInternalStreamResetStart *misrs = DP_message_internal(msg); + misrs->correlator_length = correlator_length; + if (correlator_length != 0) { + memcpy(misrs->correlator, correlator, correlator_length); + } + misrs->correlator[correlator_length] = '\0'; + return msg; +} + DP_MsgInternal *DP_msg_internal_cast(DP_Message *msg) { @@ -294,3 +318,16 @@ DP_Message **DP_msg_internal_dump_command_messages(DP_MsgInternal *mi, } return midc->messages; } + +const char *DP_msg_internal_stream_reset_start_correlator(DP_MsgInternal *mi, + size_t *out_length) +{ + DP_ASSERT(mi); + DP_ASSERT(mi->type == DP_MSG_INTERNAL_TYPE_STREAM_RESET_START); + DP_MsgInternalStreamResetStart *misrs = + (DP_MsgInternalStreamResetStart *)mi; + if (out_length) { + *out_length = misrs->correlator_length; + } + return misrs->correlator; +} diff --git a/src/drawdance/libmsg/dpmsg/msg_internal.h b/src/drawdance/libmsg/dpmsg/msg_internal.h index dfc5dabc22..5b747922eb 100644 --- a/src/drawdance/libmsg/dpmsg/msg_internal.h +++ b/src/drawdance/libmsg/dpmsg/msg_internal.h @@ -38,6 +38,7 @@ typedef enum DP_MsgInternalType { DP_MSG_INTERNAL_TYPE_DUMP_COMMAND, DP_MSG_INTERNAL_TYPE_LOCAL_FORK_CLEAR, DP_MSG_INTERNAL_TYPE_FLUSH, + DP_MSG_INTERNAL_TYPE_STREAM_RESET_START, DP_MSG_INTERNAL_TYPE_COUNT, } DP_MsgInternalType; @@ -74,6 +75,10 @@ DP_Message *DP_msg_internal_local_fork_clear_new(unsigned int context_id); DP_Message *DP_msg_internal_flush_new(unsigned int context_id); +DP_Message *DP_msg_internal_stream_reset_start_new(unsigned int context_id, + size_t correlator_length, + const char *correlator); + DP_MsgInternal *DP_msg_internal_cast(DP_Message *msg); @@ -96,5 +101,8 @@ int DP_msg_internal_dump_command_type(DP_MsgInternal *mi); DP_Message **DP_msg_internal_dump_command_messages(DP_MsgInternal *mi, int *out_count); +const char *DP_msg_internal_stream_reset_start_correlator(DP_MsgInternal *mi, + size_t *out_length); + #endif diff --git a/src/drawdance/libmsg/dpmsg/reset_stream.c b/src/drawdance/libmsg/dpmsg/reset_stream.c new file mode 100644 index 0000000000..1b708eac57 --- /dev/null +++ b/src/drawdance/libmsg/dpmsg/reset_stream.c @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "reset_stream.h" +#include "message.h" +#include "message_queue.h" +#include +#include +#include +#include +#include + + +struct DP_ResetStreamProducer { + z_stream stream; + size_t image_size; + DP_Vector msgs; + struct { + size_t capacity; + size_t used; + unsigned char *buffer; + } in; + struct { + unsigned char buffer[DP_MSG_RESET_STREAM_DATA_MAX_SIZE]; + } out; +}; + + +static voidpf malloc_z(DP_UNUSED voidpf opaque, uInt items, uInt size) +{ + return DP_malloc((size_t)items * (size_t)size); +} + +static void free_z(DP_UNUSED voidpf opaque, voidpf address) +{ + DP_free(address); +} + +static const char *get_z_error(z_stream *stream) +{ + const char *msg = stream->msg; + return msg ? msg : "no error message"; +} + + +static const char *reset_stream_producer_z_error(DP_ResetStreamProducer *rsp) +{ + return get_z_error(&rsp->stream); +} + +DP_ResetStreamProducer *DP_reset_stream_producer_new(void) +{ + DP_ResetStreamProducer *rsp = DP_malloc(sizeof(*rsp)); + *rsp = (DP_ResetStreamProducer){{Z_NULL, 0, 0, Z_NULL, 0, 0, Z_NULL, Z_NULL, + malloc_z, free_z, rsp, Z_BINARY, 0, 0}, + 0, + DP_VECTOR_NULL, + {0, 0, NULL}, + {{0}}}; + int ret = deflateInit(&rsp->stream, 9); + if (ret != Z_OK) { + DP_error_set("Deflate init error %d: %s", ret, + reset_stream_producer_z_error(rsp)); + DP_free(rsp); + return NULL; + } + rsp->stream.avail_out = (uInt)sizeof(rsp->out.buffer); + rsp->stream.next_out = rsp->out.buffer; + DP_message_vector_init(&rsp->msgs, 1024); + return rsp; +} + +void DP_reset_stream_producer_free_discard(DP_ResetStreamProducer *rsp) +{ + if (rsp) { + DP_free(rsp->in.buffer); + DP_message_vector_dispose(&rsp->msgs); + int ret = deflateEnd(&rsp->stream); + if (ret != Z_OK) { + DP_warn("Deflate end error %d: %s", ret, + reset_stream_producer_z_error(rsp)); + } + DP_free(rsp); + } +} + +static void reset_stream_producer_set_reset_data(size_t size, + unsigned char *out, void *user) +{ + DP_ResetStreamProducer *rsp = user; + memcpy(out, rsp->out.buffer, size); +} + +static void reset_stream_producer_flush_to_message(DP_ResetStreamProducer *rsp) +{ + size_t size = sizeof(rsp->out.buffer) - rsp->stream.avail_out; + if (size > 0) { + DP_Message *msg = DP_msg_reset_stream_new( + 0, reset_stream_producer_set_reset_data, size, rsp); + DP_message_vector_push_noinc(&rsp->msgs, msg); + rsp->stream.avail_out = (uInt)sizeof(rsp->out.buffer); + rsp->stream.next_out = rsp->out.buffer; + } +} + +DP_Message **DP_reset_stream_producer_free_finish(DP_ResetStreamProducer *rsp, + int *out_count) +{ + DP_ASSERT(rsp); + + while (true) { + rsp->stream.avail_in = (uInt)rsp->in.used; + rsp->stream.next_in = rsp->in.buffer; + int ret = deflate(&rsp->stream, Z_FINISH); + if (ret == Z_STREAM_END) { + reset_stream_producer_flush_to_message(rsp); + break; + } + else if (ret == Z_OK) { + reset_stream_producer_flush_to_message(rsp); + } + else { + DP_error_set("Deflate finish error %d: %s", ret, + reset_stream_producer_z_error(rsp)); + DP_reset_stream_producer_free_discard(rsp); + return NULL; + } + } + + DP_Message **msgs = rsp->msgs.elements; + if (out_count) { + *out_count = DP_size_to_int(rsp->msgs.used); + } + rsp->msgs = DP_VECTOR_NULL; + + DP_reset_stream_producer_free_discard(rsp); + return msgs; +} + +static unsigned char *stream_producer_get_serialize_buffer(void *user, + size_t size) +{ + DP_ResetStreamProducer *rsp = user; + size_t used = rsp->in.used; + size_t required_size = used + size; + if (rsp->in.capacity < required_size) { + DP_free(rsp->in.buffer); + rsp->in.buffer = DP_malloc(required_size); + rsp->in.capacity = required_size; + } + return rsp->in.buffer + used; +} + +size_t DP_reset_stream_producer_image_size(DP_ResetStreamProducer *rsp) +{ + DP_ASSERT(rsp); + return rsp->image_size; +} + +bool DP_reset_stream_producer_push(DP_ResetStreamProducer *rsp, DP_Message *msg) +{ + DP_ASSERT(rsp); + DP_ASSERT(msg); + + size_t size = DP_message_serialize( + msg, true, stream_producer_get_serialize_buffer, rsp); + if (size == 0) { + return false; + } + + rsp->image_size += size; + rsp->in.used += size; + rsp->stream.avail_in = (uInt)rsp->in.used; + rsp->stream.next_in = rsp->in.buffer; + + while (true) { + DP_ASSERT(rsp->stream.avail_out != 0); + int ret = deflate(&rsp->stream, Z_NO_FLUSH); + if (ret != Z_OK) { + DP_error_set("Deflate error %d: %s", ret, + reset_stream_producer_z_error(rsp)); + return false; + } + + if (rsp->stream.avail_out == 0) { + reset_stream_producer_flush_to_message(rsp); + unsigned int pending; + ret = deflatePending(&rsp->stream, &pending, Z_NULL); + if (ret != Z_OK) { + DP_error_set("Deflate pending error %d: %s", ret, + reset_stream_producer_z_error(rsp)); + return false; + } + else if (pending == 0) { + break; + } + } + else { + break; + } + } + + size_t used = rsp->in.used; + size_t remaining = rsp->stream.avail_in; + rsp->in.used = remaining; + if (remaining != 0) { + memmove(rsp->in.buffer, rsp->in.buffer + used - remaining, remaining); + } + return true; +} + + +struct DP_ResetStreamConsumer { + z_stream stream; + DP_ResetStreamConsumerMessageFn fn; + void *user; + bool decode_opaque; + unsigned char buffer[((size_t)DP_MESSAGE_HEADER_LENGTH + + (size_t)DP_MESSAGE_MAX_PAYLOAD_LENGTH) + * (size_t)2]; +}; + + +static const char *reset_stream_consumer_z_error(DP_ResetStreamConsumer *rsc) +{ + return get_z_error(&rsc->stream); +} + +DP_ResetStreamConsumer * +DP_reset_stream_consumer_new(DP_ResetStreamConsumerMessageFn fn, void *user, + bool decode_opaque) +{ + DP_ASSERT(fn); + DP_ResetStreamConsumer *rsc = DP_malloc(sizeof(*rsc)); + *rsc = (DP_ResetStreamConsumer){{Z_NULL, 0, 0, rsc->buffer, + sizeof(rsc->buffer), 0, Z_NULL, Z_NULL, + malloc_z, free_z, rsc, 0, 0, 0}, + fn, + user, + decode_opaque, + {0}}; + int ret = inflateInit(&rsc->stream); + if (ret != Z_OK) { + DP_error_set("Inflate init error %d: %s", ret, + reset_stream_consumer_z_error(rsc)); + DP_free(rsc); + return NULL; + } + return rsc; +} + +void DP_reset_stream_consumer_free_discard(DP_ResetStreamConsumer *rsc) +{ + if (rsc) { + int ret = inflateEnd(&rsc->stream); + if (ret != Z_OK) { + DP_warn("Inflate end error %d: %s", ret, + reset_stream_consumer_z_error(rsc)); + } + DP_free(rsc); + } +} + +static bool reset_stream_consumer_flush_messages(DP_ResetStreamConsumer *rsc) +{ + size_t size = sizeof(rsc->buffer) - (size_t)rsc->stream.avail_out; + size_t remaining = size; + size_t offset = 0; + bool ok = true; + while (remaining >= DP_MESSAGE_HEADER_LENGTH) { + size_t payload_length = DP_read_bigendian_uint16(rsc->buffer + offset); + size_t message_length = DP_MESSAGE_HEADER_LENGTH + payload_length; + if (remaining >= message_length) { + DP_Message *msg = DP_message_deserialize( + rsc->buffer + offset, remaining, rsc->decode_opaque); + remaining -= message_length; + offset += message_length; + ok = msg && rsc->fn(rsc->user, msg); + } + else { + break; + } + } + + if (offset != 0) { + memmove(rsc->buffer, rsc->buffer + offset, remaining); + rsc->stream.avail_out = (uInt)(sizeof(rsc->buffer) - remaining); + rsc->stream.next_out = rsc->buffer + remaining; + } + + return ok; +} + +bool DP_reset_stream_consumer_free_finish(DP_ResetStreamConsumer *rsc) +{ + rsc->stream.next_in = Z_NULL; + rsc->stream.avail_in = 0; + + while (true) { + int ret = inflate(&rsc->stream, Z_FINISH); + if (ret == Z_STREAM_END) { + if (!reset_stream_consumer_flush_messages(rsc)) { + DP_reset_stream_consumer_free_discard(rsc); + return false; + } + break; + } + else if (ret == Z_OK) { + if (!reset_stream_consumer_flush_messages(rsc)) { + DP_reset_stream_consumer_free_discard(rsc); + return false; + } + } + else { + DP_error_set("Inflate finish error %d: %s", ret, + reset_stream_consumer_z_error(rsc)); + DP_reset_stream_consumer_free_discard(rsc); + return false; + } + } + + if (rsc->stream.avail_out != sizeof(rsc->buffer)) { + DP_error_set("Leftover data from inflate"); + DP_reset_stream_consumer_free_discard(rsc); + return false; + } + + DP_reset_stream_consumer_free_discard(rsc); + return true; +} + +bool DP_reset_stream_consumer_push(DP_ResetStreamConsumer *rsc, + const void *data, size_t size) +{ + if (data && size != 0) { + rsc->stream.next_in = (Bytef *)data; + rsc->stream.avail_in = (uInt)size; + do { + int ret = inflate(&rsc->stream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_STREAM_END) { + DP_warn("Inflate error %d: %s", ret, + reset_stream_consumer_z_error(rsc)); + return false; + } + + if (!reset_stream_consumer_flush_messages(rsc)) { + return false; + } + } while (rsc->stream.avail_in > 0); + } + return true; +} diff --git a/src/drawdance/libmsg/dpmsg/reset_stream.h b/src/drawdance/libmsg/dpmsg/reset_stream.h new file mode 100644 index 0000000000..3e1bcd8f1d --- /dev/null +++ b/src/drawdance/libmsg/dpmsg/reset_stream.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef DPMSG_RESET_STREAM_H +#define DPMSG_RESET_STREAM_H +#include + +typedef struct DP_Message DP_Message; + + +typedef struct DP_ResetStreamProducer DP_ResetStreamProducer; + +DP_ResetStreamProducer *DP_reset_stream_producer_new(void); + +void DP_reset_stream_producer_free_discard(DP_ResetStreamProducer *rsp); + +DP_Message **DP_reset_stream_producer_free_finish(DP_ResetStreamProducer *rsp, + int *out_count); + +// Size of the reset image in bytes thus far. +size_t DP_reset_stream_producer_image_size(DP_ResetStreamProducer *rsp); + +bool DP_reset_stream_producer_push(DP_ResetStreamProducer *rsp, + DP_Message *msg); + + +typedef struct DP_ResetStreamConsumer DP_ResetStreamConsumer; + +typedef bool (*DP_ResetStreamConsumerMessageFn)(void *user, DP_Message *msg); + +DP_ResetStreamConsumer * +DP_reset_stream_consumer_new(DP_ResetStreamConsumerMessageFn fn, void *user, + bool decode_opaque); + +void DP_reset_stream_consumer_free_discard(DP_ResetStreamConsumer *rsc); + +bool DP_reset_stream_consumer_free_finish(DP_ResetStreamConsumer *rsc); + +bool DP_reset_stream_consumer_push(DP_ResetStreamConsumer *rsc, + const void *data, size_t size); + + +#endif diff --git a/src/drawdance/libmsg/test/read_write_roundtrip.c b/src/drawdance/libmsg/test/read_write_roundtrip.c index 47da6298e7..e6980eb22e 100644 --- a/src/drawdance/libmsg/test/read_write_roundtrip.c +++ b/src/drawdance/libmsg/test/read_write_roundtrip.c @@ -22,10 +22,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -806,71 +809,72 @@ static DP_Message *generate_undo(void) typedef DP_Message *(*GenerateFn)(void); +static GenerateFn generate_fns[] = { + generate_server_command, + generate_disconnect, + generate_ping, + generate_join, + generate_leave, + generate_session_owner, + generate_trusted_users, + generate_soft_reset, + generate_private_chat, + generate_interval, + generate_laser_trail, + generate_move_pointer, + generate_marker, + generate_user_acl, + generate_layer_acl, + generate_feature_access_levels, + generate_default_layer, + generate_filtered, + generate_undo_depth, + generate_data, + generate_local_change, + generate_undo_point, + generate_canvas_resize, + generate_layer_create, + generate_layer_attributes, + generate_layer_retitle, + generate_layer_order, + generate_layer_delete, + generate_layer_visibility, + generate_put_image, + generate_fill_rect, + generate_pen_up, + generate_annotation_create, + generate_annotation_reshape, + generate_annotation_edit, + generate_annotation_delete, + generate_move_region, + generate_put_tile, + generate_canvas_background, + generate_draw_dabs_classic, + generate_draw_dabs_pixel, + generate_draw_dabs_pixel_square, + generate_draw_dabs_mypaint, + generate_move_rect, + generate_set_metadata_int, + generate_layer_tree_create, + generate_layer_tree_move, + generate_layer_tree_delete, + generate_transform_region, + generate_track_create, + generate_track_retitle, + generate_track_delete, + generate_track_order, + generate_key_frame_set, + generate_key_frame_retitle, + generate_key_frame_layer_attributes, + generate_key_frame_delete, + generate_undo, +}; + static void write_messages(TEST_PARAMS, DP_BinaryWriter *bw, DP_TextWriter *tw) { - GenerateFn fns[] = { - generate_server_command, - generate_disconnect, - generate_ping, - generate_join, - generate_leave, - generate_session_owner, - generate_trusted_users, - generate_soft_reset, - generate_private_chat, - generate_interval, - generate_laser_trail, - generate_move_pointer, - generate_marker, - generate_user_acl, - generate_layer_acl, - generate_feature_access_levels, - generate_default_layer, - generate_filtered, - generate_undo_depth, - generate_data, - generate_local_change, - generate_undo_point, - generate_canvas_resize, - generate_layer_create, - generate_layer_attributes, - generate_layer_retitle, - generate_layer_order, - generate_layer_delete, - generate_layer_visibility, - generate_put_image, - generate_fill_rect, - generate_pen_up, - generate_annotation_create, - generate_annotation_reshape, - generate_annotation_edit, - generate_annotation_delete, - generate_move_region, - generate_put_tile, - generate_canvas_background, - generate_draw_dabs_classic, - generate_draw_dabs_pixel, - generate_draw_dabs_pixel_square, - generate_draw_dabs_mypaint, - generate_move_rect, - generate_set_metadata_int, - generate_layer_tree_create, - generate_layer_tree_move, - generate_layer_tree_delete, - generate_transform_region, - generate_track_create, - generate_track_retitle, - generate_track_delete, - generate_track_order, - generate_key_frame_set, - generate_key_frame_retitle, - generate_key_frame_layer_attributes, - generate_key_frame_delete, - generate_undo, - }; - int count = DP_ARRAY_LENGTH(fns); + int count = DP_ARRAY_LENGTH(generate_fns); for (int i = 0; i < count; ++i) { - DP_Message *msg = fns[i](); + DP_Message *msg = generate_fns[i](); write_message_binary(TEST_ARGS, msg, bw); write_message_text(TEST_ARGS, msg, tw); DP_message_decref(msg); @@ -1025,9 +1029,112 @@ static void read_write_roundtrip(TEST_PARAMS) } +static bool push_consumer_message(void *user, DP_Message *msg) +{ + DP_Vector *out_msgs = user; + DP_message_vector_push_noinc(out_msgs, msg); + return true; +} + +static void free_stream_messages(int count, DP_Message **msgs) +{ + for (int i = 0; i < count; ++i) { + DP_message_decref(msgs[i]); + } + DP_free(msgs); +} + +static void reset_stream_roundtrip(TEST_PARAMS) +{ + size_t count = (size_t)1 + (size_t)random_uint32() % (size_t)1000; + NOTE("Testing round-trip with %zu message(s)", count); + + DP_Vector in_msgs; + DP_message_vector_init(&in_msgs, count); + for (size_t i = 0; i < count; ++i) { + DP_Message *msg = generate_fns[DP_uint32_to_size(random_uint32()) + % DP_ARRAY_LENGTH(generate_fns)](); + DP_message_vector_push_noinc(&in_msgs, msg); + } + + DP_ResetStreamProducer *rsp = DP_reset_stream_producer_new(); + if (!NOT_NULL_OK(rsp, "producer created")) { + DP_message_vector_dispose(&in_msgs); + return; + } + + for (size_t i = 0; i < count; ++i) { + if (!OK(DP_reset_stream_producer_push( + rsp, DP_message_vector_at(&in_msgs, i)), + "push producer message %zu", i)) { + DP_reset_stream_producer_free_discard(rsp); + DP_message_vector_dispose(&in_msgs); + return; + } + } + + int stream_count; + DP_Message **stream_msgs = + DP_reset_stream_producer_free_finish(rsp, &stream_count); + if (!OK(stream_msgs, "producer finished") + || !OK(stream_count > 0, "stream message count %d", stream_count)) { + DP_message_vector_dispose(&in_msgs); + return; + } + + DP_Vector out_msgs; + DP_message_vector_init(&out_msgs, count); + + DP_ResetStreamConsumer *rsc = + DP_reset_stream_consumer_new(push_consumer_message, &out_msgs, true); + if (!NOT_NULL_OK(rsc, "consumer created")) { + DP_message_vector_dispose(&out_msgs); + free_stream_messages(stream_count, stream_msgs); + DP_message_vector_dispose(&in_msgs); + return; + } + + for (int i = 0; i < stream_count; ++i) { + DP_Message *msg = stream_msgs[i]; + if (INT_EQ_OK(DP_message_type(msg), DP_MSG_RESET_STREAM, + "stream message %d is reset stream", i)) { + size_t size; + const unsigned char *data = + DP_msg_reset_stream_data(DP_message_internal(msg), &size); + if (OK(data && size != 0, "stream message %d has data", i)) { + if (!OK(DP_reset_stream_consumer_push(rsc, data, size), + "push consumer message %d", i)) { + DP_reset_stream_consumer_free_discard(rsc); + DP_message_vector_dispose(&out_msgs); + free_stream_messages(stream_count, stream_msgs); + DP_message_vector_dispose(&in_msgs); + return; + } + } + } + } + + free_stream_messages(stream_count, stream_msgs); + if (OK(DP_reset_stream_consumer_free_finish(rsc), "free consumer")) { + if (UINT_EQ_OK(out_msgs.used, in_msgs.used, "message counts match")) { + for (size_t i = 0; i < in_msgs.used; ++i) { + DP_Message *in_msg = DP_message_vector_at(&in_msgs, i); + DP_Message *out_msg = DP_message_vector_at(&in_msgs, i); + OK(DP_message_equals(out_msg, in_msg), "message %zu is equal", + i); + } + } + } + + DP_message_vector_dispose(&out_msgs); + DP_message_vector_dispose(&in_msgs); +} + + static void register_tests(REGISTER_PARAMS) { REGISTER_TEST(read_write_roundtrip); + REGISTER_TEST(reset_stream_roundtrip); } int main(int argc, char **argv) diff --git a/src/drawdance/rust/bindings.rs b/src/drawdance/rust/bindings.rs index 8e3fc3d186..0f7d087b75 100644 --- a/src/drawdance/rust/bindings.rs +++ b/src/drawdance/rust/bindings.rs @@ -100,6 +100,9 @@ pub const DP_MSG_SOFT_RESET_STATIC_LENGTH: u32 = 0; pub const DP_MSG_PRIVATE_CHAT_STATIC_LENGTH: u32 = 2; pub const DP_MSG_PRIVATE_CHAT_MESSAGE_MIN_LEN: u32 = 0; pub const DP_MSG_PRIVATE_CHAT_MESSAGE_MAX_LEN: u32 = 65533; +pub const DP_MSG_RESET_STREAM_STATIC_LENGTH: u32 = 0; +pub const DP_MSG_RESET_STREAM_DATA_MIN_SIZE: u32 = 0; +pub const DP_MSG_RESET_STREAM_DATA_MAX_SIZE: u32 = 65535; pub const DP_MSG_INTERVAL_STATIC_LENGTH: u32 = 2; pub const DP_MSG_LASER_TRAIL_STATIC_LENGTH: u32 = 5; pub const DP_MSG_MOVE_POINTER_STATIC_LENGTH: u32 = 8; @@ -4924,6 +4927,12 @@ extern "C" { user: *mut ::std::os::raw::c_void, ); } +extern "C" { + pub fn DP_canvas_history_stream_start_state_inc( + ch: *mut DP_CanvasHistory, + dc: *mut DP_DrawContext, + ) -> *mut DP_CanvasState; +} extern "C" { pub fn DP_canvas_history_undo_depth_limit(ch: *mut DP_CanvasHistory) -> ::std::os::raw::c_int; } @@ -6397,6 +6406,14 @@ pub type DP_PaintEngineDumpPlaybackFn = ::std::option::Option< chs: *mut DP_CanvasHistorySnapshot, ), >; +pub type DP_PaintEngineStreamResetStartFn = ::std::option::Option< + unsafe extern "C" fn( + user: *mut ::std::os::raw::c_void, + cs: *mut DP_CanvasState, + correlator_length: usize, + correlator: *const ::std::os::raw::c_char, + ), +>; pub type DP_PaintEngineAclsChangedFn = ::std::option::Option< unsafe extern "C" fn( user: *mut ::std::os::raw::c_void, @@ -6589,6 +6606,8 @@ extern "C" { playback_fn: DP_PaintEnginePlaybackFn, dump_playback_fn: DP_PaintEngineDumpPlaybackFn, playback_user: *mut ::std::os::raw::c_void, + stream_reset_start_fn: DP_PaintEngineStreamResetStartFn, + stream_reset_user: *mut ::std::os::raw::c_void, ) -> *mut DP_PaintEngine; } extern "C" { @@ -8480,6 +8499,7 @@ pub const DP_MSG_CHAT: DP_MessageType = 35; pub const DP_MSG_TRUSTED_USERS: DP_MessageType = 36; pub const DP_MSG_SOFT_RESET: DP_MessageType = 37; pub const DP_MSG_PRIVATE_CHAT: DP_MessageType = 38; +pub const DP_MSG_RESET_STREAM: DP_MessageType = 39; pub const DP_MSG_INTERVAL: DP_MessageType = 64; pub const DP_MSG_LASER_TRAIL: DP_MessageType = 65; pub const DP_MSG_MOVE_POINTER: DP_MessageType = 66; @@ -9001,6 +9021,50 @@ extern "C" { } #[repr(C)] #[derive(Debug, Copy, Clone)] +pub struct DP_MsgResetStream { + _unused: [u8; 0], +} +extern "C" { + pub fn DP_msg_reset_stream_new( + context_id: ::std::os::raw::c_uint, + set_data: ::std::option::Option< + unsafe extern "C" fn( + arg1: usize, + arg2: *mut ::std::os::raw::c_uchar, + arg3: *mut ::std::os::raw::c_void, + ), + >, + data_size: usize, + data_user: *mut ::std::os::raw::c_void, + ) -> *mut DP_Message; +} +extern "C" { + pub fn DP_msg_reset_stream_deserialize( + context_id: ::std::os::raw::c_uint, + buffer: *const ::std::os::raw::c_uchar, + length: usize, + ) -> *mut DP_Message; +} +extern "C" { + pub fn DP_msg_reset_stream_parse( + context_id: ::std::os::raw::c_uint, + reader: *mut DP_TextReader, + ) -> *mut DP_Message; +} +extern "C" { + pub fn DP_msg_reset_stream_cast(msg: *mut DP_Message) -> *mut DP_MsgResetStream; +} +extern "C" { + pub fn DP_msg_reset_stream_data( + mrs: *const DP_MsgResetStream, + out_size: *mut usize, + ) -> *const ::std::os::raw::c_uchar; +} +extern "C" { + pub fn DP_msg_reset_stream_data_size(mrs: *const DP_MsgResetStream) -> usize; +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] pub struct DP_MsgInterval { _unused: [u8; 0], } diff --git a/src/drawdance/rust/engine/paint_engine.rs b/src/drawdance/rust/engine/paint_engine.rs index 6dd143e3d5..82f3dde6f9 100644 --- a/src/drawdance/rust/engine/paint_engine.rs +++ b/src/drawdance/rust/engine/paint_engine.rs @@ -88,6 +88,8 @@ impl PaintEngine { Some(Self::on_playback), None, user.cast(), + None, + ptr::null_mut(), ) }; pe diff --git a/src/libclient/canvas/canvasmodel.cpp b/src/libclient/canvas/canvasmodel.cpp index 87b2dfa672..6f694cb104 100644 --- a/src/libclient/canvas/canvasmodel.cpp +++ b/src/libclient/canvas/canvasmodel.cpp @@ -312,14 +312,16 @@ net::MessageList CanvasModel::generateSnapshot( return snapshot; } -void CanvasModel::amendSnapshotMetadata( +int CanvasModel::amendSnapshotMetadata( net::MessageList &snapshot, bool includePinnedMessage, unsigned int aclIncludeFlags) const { + int prepended = 1; snapshot.prepend( net::makeUndoDepthMessage(0, m_paintengine->undoDepthLimit())); if(includePinnedMessage && !m_pinnedMessage.isEmpty()) { + ++prepended; snapshot.prepend(net::makeChatMessage( m_localUserId, 0, DP_MSG_CHAT_OFLAGS_PIN, m_pinnedMessage)); } @@ -331,6 +333,8 @@ void CanvasModel::amendSnapshotMetadata( m_paintengine->aclState().toResetImage( snapshot, m_localUserId, aclIncludeFlags); + + return prepended; } void CanvasModel::pickLayer(int x, int y) diff --git a/src/libclient/canvas/canvasmodel.h b/src/libclient/canvas/canvasmodel.h index 0cec018c3a..dd69e13329 100644 --- a/src/libclient/canvas/canvasmodel.h +++ b/src/libclient/canvas/canvasmodel.h @@ -67,7 +67,8 @@ class CanvasModel final : public QObject { net::MessageList generateSnapshot( bool includePinnedMessage, unsigned int aclIncludeFlags) const; - void amendSnapshotMetadata( + // Returns the number of messages prepended, the rest are appended. + int amendSnapshotMetadata( net::MessageList &snapshot, bool includePinnedMessage, unsigned int aclIncludeFlags) const; diff --git a/src/libclient/canvas/paintengine.cpp b/src/libclient/canvas/paintengine.cpp index 63bdcd4102..72d3e2fa65 100644 --- a/src/libclient/canvas/paintengine.cpp +++ b/src/libclient/canvas/paintengine.cpp @@ -61,7 +61,8 @@ PaintEngine::PaintEngine( m_useTileCache ? PaintEngine::onRenderResizeTileCache : PaintEngine::onRenderResizePixmap, this, ON_SOFT_RESET_FN, this, PaintEngine::onPlayback, - PaintEngine::onDumpPlayback, this) + PaintEngine::onDumpPlayback, this, PaintEngine::onStreamResetStart, + this) , m_fps{fps} , m_timerId{0} , m_cache{} @@ -128,7 +129,8 @@ void PaintEngine::reset( m_useTileCache ? PaintEngine::onRenderResizeTileCache : PaintEngine::onRenderResizePixmap, this, ON_SOFT_RESET_FN, this, PaintEngine::onPlayback, - PaintEngine::onDumpPlayback, this, canvasState, player); + PaintEngine::onDumpPlayback, this, PaintEngine::onStreamResetStart, + this, canvasState, player); DP_mutex_lock(m_cacheMutex); if(m_useTileCache) { m_tileCache.clear(); @@ -816,6 +818,18 @@ void PaintEngine::onDumpPlayback( position, drawdance::CanvasHistorySnapshot::inc(chs)); } +void PaintEngine::onStreamResetStart( + void *user, DP_CanvasState *cs, size_t correlatorLength, + const char *correlator) +{ + PaintEngine *pe = static_cast(user); + emit pe->streamResetStarted( + drawdance::CanvasState::noinc(cs), + correlator ? QString::fromUtf8( + QByteArray::fromRawData(correlator, correlatorLength)) + : QString()); +} + void PaintEngine::onAclsChanged(void *user, int aclChangeFlags) { PaintEngine *pe = static_cast(user); diff --git a/src/libclient/canvas/paintengine.h b/src/libclient/canvas/paintengine.h index 9f62dab42a..bbd45aa1dd 100644 --- a/src/libclient/canvas/paintengine.h +++ b/src/libclient/canvas/paintengine.h @@ -268,6 +268,8 @@ class PaintEngine final : public QObject { void playbackAt(long long pos); void dumpPlaybackAt(long long pos, const drawdance::CanvasHistorySnapshot &chs); + void streamResetStarted( + const drawdance::CanvasState &cs, const QString &correlator); void caughtUpTo(int progress); void resetLockSet(bool locked); void recorderStateChanged(bool started); @@ -297,6 +299,9 @@ private slots: static void onPlayback(void *user, long long position); static void onDumpPlayback( void *user, long long position, DP_CanvasHistorySnapshot *chs); + static void onStreamResetStart( + void *user, DP_CanvasState *cs, size_t correlatorLength, + const char *correlator); static void onAclsChanged(void *user, int aclChangeFlags); static void onLaserTrail( void *user, unsigned int contextId, int persistence, uint32_t color); diff --git a/src/libclient/document.cpp b/src/libclient/document.cpp index 62a00fca9f..698763bb0c 100644 --- a/src/libclient/document.cpp +++ b/src/libclient/document.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later extern "C" { +#include #include +#include } #include "libclient/canvas/canvasmodel.h" #include "libclient/canvas/layerlist.h" @@ -109,9 +111,14 @@ Document::Document( connect( m_client, &net::Client::needSnapshot, this, &Document::snapshotNeeded); + connect( + m_client, &net::Client::lagMeasured, this, &Document::onLagMeasured); connect( m_client, &net::Client::sessionConfChange, this, &Document::onSessionConfChanged); + connect( + m_client, &net::Client::autoresetQueried, this, + &Document::onAutoresetQueried); connect( m_client, &net::Client::autoresetRequested, this, &Document::onAutoresetRequested); @@ -121,6 +128,12 @@ Document::Document( connect( m_client, &net::Client::sessionResetted, this, &Document::onSessionResetted); + connect( + m_client, &net::Client::streamResetStarted, this, + &Document::onStreamResetStarted); + connect( + m_client, &net::Client::streamResetProgressed, this, + &Document::onStreamResetProgressed); connect( m_client, &net::Client::sessionOutOfSpace, this, &Document::onSessionOutOfSpace); @@ -128,6 +141,9 @@ Document::Document( connect( this, &Document::justInTimeSnapshotGenerated, this, &Document::sendResetSnapshot, Qt::QueuedConnection); + connect( + this, &Document::buildStreamResetImageFinished, this, + &Document::startSendingStreamResetSnapshot, Qt::QueuedConnection); } void Document::initCanvas() @@ -160,6 +176,9 @@ void Document::initCanvas() m_canvas, &canvas::CanvasModel::recorderStateChanged, this, &Document::recorderStateChanged); + connect( + m_canvas->paintEngine(), &canvas::PaintEngine::streamResetStarted, this, + &Document::buildStreamResetImage, Qt::QueuedConnection); connect( m_client, &net::Client::catchupProgress, m_canvas->paintEngine(), &canvas::PaintEngine::enqueueCatchupProgress); @@ -312,6 +331,9 @@ void Document::onServerDisconnect() setSessionOutOfSpace(false); emit compatibilityModeChanged(false); setPreparingReset(false); + m_autoResetCorrelator = QString(); + setStreamResetState(StreamResetState::None); + m_pingHistory.clear(); } void Document::setPreparingReset(bool preparing) @@ -322,6 +344,16 @@ void Document::setPreparingReset(bool preparing) } } +void Document::onLagMeasured(qint64 msec) +{ + if(m_client->isConnected()) { + while(m_pingHistory.size() >= 64) { + m_pingHistory.dequeue(); + } + m_pingHistory.enqueue(qMax(qint64(1), msec)); + } +} + void Document::onSessionConfChanged(const QJsonObject &config) { if(config.contains("persistent")) @@ -406,42 +438,142 @@ void Document::onSessionConfChanged(const QJsonObject &config) } } -void Document::onAutoresetRequested(int maxSize, bool query) +void Document::onAutoresetQueried(int maxSize, const QString &payload) { Q_ASSERT(m_canvas); - - qInfo("Server requested autoreset (query=%d)", query); - - if(!m_client->isFullyCaughtUp()) { - qInfo("Ignoring autoreset request because we're not fully caught up " - "yet."); - return; - } - + qDebug("Server queried autoreset"); m_sessionHistoryMaxSize = maxSize; - - if(m_settings.serverAutoReset()) { - if(query) { - // This is just a query: send back an affirmative response + m_autoResetCorrelator.clear(); + + if(shouldRespondToAutoReset()) { + // Users can self-indicate in their network preferences that their + // connection sucks and they don't want to be considered for autoresets. + // This used to entirely disable autoresets, but of course that can + // instead get you into a situation where the session runs out of space + // or just takes forever to catch up to. The current behavior is for it + // to communicate a low network quality to the server if it understands + // that or just put in a big delay in responding to the autoreset + // request, which should result in us not getting picked over others. + bool serverAutoReset = m_settings.serverAutoReset(); + // Presence of a payload indicates that the server supports extended + // autoreset capabilities introduced in Drawpile 2.2.2. + if(!payload.isEmpty()) { + QJsonArray pings; + for(qint64 ping : m_pingHistory) { + pings.append(qreal(ping) / 1000.0); + } + QJsonObject kwargs = { + {QStringLiteral("payload"), payload}, + {QStringLiteral("capabilities"), + QJsonArray({QStringLiteral("gzip1")})}, + {QStringLiteral("os"), net::ServerCommand::autoresetOs()}, + {QStringLiteral("net"), serverAutoReset ? 0.0 : 100.0}, + {QStringLiteral("pings"), pings}, + }; + m_client->sendMessage(net::ServerCommand::make( + QStringLiteral("ready-to-autoreset"), {}, kwargs)); + } else if(serverAutoReset) { m_client->sendMessage( - net::ServerCommand::make("ready-to-autoreset")); + net::ServerCommand::make(QStringLiteral("ready-to-autoreset"))); + } else { + QTimer::singleShot(10000, Qt::VeryCoarseTimer, this, [this] { + if(m_client->isConnected()) { + m_client->sendMessage(net::ServerCommand::make( + QStringLiteral("ready-to-autoreset"))); + } + }); + } + } +} +void Document::onAutoresetRequested( + int maxSize, const QString &correlator, const QString &stream) +{ + Q_ASSERT(m_canvas); + qDebug("Server requested autoreset"); + m_sessionHistoryMaxSize = maxSize; + m_autoResetCorrelator = correlator; + + if(shouldRespondToAutoReset()) { + if(stream == QStringLiteral("gzip1")) { + // Streamed autoreset. Keep going as normal, send reset messages in + // the background so the server can swap out its state on the fly. + qDebug("Send stream-reset-start"); + m_client->sendMessage(net::ServerCommand::make( + QStringLiteral("stream-reset-start"), {m_autoResetCorrelator})); } else { - // Autoreset on request - // TODO: There seems to be a race condition in the server where it - // will fail to actually send this command out to the clients if the - // subsequent server command comes in too fast. + // Interrupting autoreset. Stop everything and send a reset image. + // TODO: There seems to be a race condition in the server where + // it will fail to actually send this command out to the clients + // if the subsequent server command comes in too fast. sendLockSession(true); - m_client->sendMessage(net::makeChatMessage( m_client->myId(), DP_MSG_CHAT_TFLAGS_BYPASS, DP_MSG_CHAT_OFLAGS_ACTION, QStringLiteral("beginning session autoreset..."))); - sendResetSession(); } + } +} + +void Document::onStreamResetStarted(const QString &correlator) +{ + if(m_canvas && !correlator.isEmpty() && + m_autoResetCorrelator == correlator) { + net::Message msg = + net::makeInternalStreamResetStartMessage(0, correlator); + m_canvas->handleCommands(1, &msg); + } +} + +void Document::onStreamResetProgressed(bool cancel) +{ + switch(m_streamResetState) { + case StreamResetState::Streaming: + if(cancel) { + qWarning("Stream reset cancelled while streaming it"); + setStreamResetState(StreamResetState::None); + } else { + sendNextStreamResetMessage(); + } + break; + case StreamResetState::Generating: + if(cancel) { + qWarning("Stream reset cancelled while generating the reset image"); + setStreamResetState(StreamResetState::None); + } else { + qWarning("Stream reset progress requested, but we're still " + "generating the reset image"); + } + break; + default: + qWarning( + "Stream reset %s requested, but none is active", + cancel ? "cancel" : "progress"); + setStreamResetState(StreamResetState::None); + } +} + +void Document::buildStreamResetImage( + const drawdance::CanvasState &canvasState, const QString &correlator) +{ + if(correlator == m_autoResetCorrelator) { + setStreamResetState(StreamResetState::Generating); + qDebug("Building stream reset image"); + net::MessageList metadata; + int prepended = m_canvas->amendSnapshotMetadata( + metadata, true, DP_ACL_STATE_RESET_IMAGE_SESSION_RESET_FLAGS); + utils::FunctionRunnable *runnable = new utils::FunctionRunnable( + [this, canvasState, correlator, metadata, prepended]() { + int messageCount; + net::MessageList image = generateStreamSnapshot( + canvasState, metadata, prepended, messageCount); + emit buildStreamResetImageFinished( + image, messageCount, correlator); + }); + QThreadPool::globalInstance()->start(runnable); } else { - qInfo("Ignoring autoreset request as configured."); + qWarning("Stream reset start triggered, but correlator does not match"); } } @@ -963,6 +1095,16 @@ void Document::snapshotNeeded() } } +bool Document::shouldRespondToAutoReset() const +{ + if(!m_client->isFullyCaughtUp()) { + qInfo("Ignoring autoreset request because we're not caught up yet"); + return false; + } else { + return true; + } +} + void Document::generateJustInTimeSnapshot() { qInfo("Generating a just-in-time snapshot for session reset..."); @@ -997,6 +1139,156 @@ void Document::sendResetSnapshot() m_resetstate.clear(); } +namespace { +struct ResetStreamImageContext { + DP_ResetStreamProducer *rsp; + int count; + bool ok; + + static void push(void *user, DP_Message *msg) + { + ResetStreamImageContext *ctx = + static_cast(user); + if(ctx->ok) { + if(DP_reset_stream_producer_push(ctx->rsp, msg)) { + ++ctx->count; + } else { + qWarning("Reset stream: %s", DP_error()); + ctx->ok = false; + } + } + DP_message_decref(msg); + } +}; +} + +net::MessageList Document::generateStreamSnapshot( + const drawdance::CanvasState &canvasState, const net::MessageList &metadata, + int prepended, int &outMessageCount) const +{ + DP_ResetStreamProducer *rsp = DP_reset_stream_producer_new(); + if(!rsp) { + qWarning("Error initializing reset stream producer: %s", DP_error()); + return {}; + } + + for(int i = 0; i < prepended; ++i) { + if(!DP_reset_stream_producer_push(rsp, metadata[i].get())) { + qWarning("Reset stream: %s", DP_error()); + DP_reset_stream_producer_free_discard(rsp); + return {}; + } + } + + ResetStreamImageContext ctx = {rsp, 0, true}; + DP_reset_image_build( + canvasState.get(), 0, ResetStreamImageContext::push, &ctx); + if(!ctx.ok) { + DP_reset_stream_producer_free_discard(rsp); + return {}; + } + + int metadataCount = metadata.size(); + for(int i = prepended; i < metadataCount; ++i) { + if(!DP_reset_stream_producer_push(rsp, metadata[i].get())) { + qWarning("Reset stream: %s", DP_error()); + DP_reset_stream_producer_free_discard(rsp); + return {}; + } + } + + if(m_sessionHistoryMaxSize > 0 && DP_reset_stream_producer_image_size(rsp) > + size_t(m_sessionHistoryMaxSize)) { + qWarning("Reset stream: oversized reset image"); + DP_reset_stream_producer_free_discard(rsp); + return {}; + } + + int count; + DP_Message **msgs = DP_reset_stream_producer_free_finish(rsp, &count); + if(!msgs) { + qWarning("Reset stream: %s", DP_error()); + return {}; + } + + net::MessageList image; + image.reserve(count); + for(int i = 0; i < count; ++i) { + image.append(net::Message::noinc(msgs[i])); + } + DP_free(msgs); + + outMessageCount = metadataCount + ctx.count; + return image; +} + +void Document::startSendingStreamResetSnapshot( + const net::MessageList &image, int messageCount, const QString &correlator) +{ + if(m_streamResetState == StreamResetState::Generating && + m_autoResetCorrelator == correlator) { + if(messageCount > 0 && !image.isEmpty()) { + qDebug("Start sending stream reset image"); + m_streamResetImage = image; + m_streamResetImageOriginalCount = image.size(); + setStreamResetState(StreamResetState::Streaming, messageCount); + sendNextStreamResetMessage(); + } else { + qDebug("Abort sending stream reset image"); + m_streamResetImage.clear(); + m_streamResetImageOriginalCount = 0; + setStreamResetState(StreamResetState::None); + m_client->sendMessage( + net::ServerCommand::make(QStringLiteral("stream-reset-abort"))); + } + } else { + qWarning("Streamed reset image generated, but not ready to stream"); + } +} + +void Document::sendNextStreamResetMessage() +{ + if(m_streamResetState == StreamResetState::Streaming) { + if(m_streamResetImage.isEmpty()) { + qDebug("Send stream-reset-finish"); + m_client->sendMessage(net::ServerCommand::make( + QStringLiteral("stream-reset-finish"), + {m_streamResetMessageCount})); + setStreamResetState(StreamResetState::None); + } else { + m_client->sendMessage(m_streamResetImage.takeFirst()); + emitStreamResetProgress(); + } + } else { + setStreamResetState(StreamResetState::None); + } +} + +void Document::setStreamResetState(StreamResetState state, int messageCount) +{ + m_streamResetState = state; + m_streamResetMessageCount = messageCount; + emitStreamResetProgress(); +} + +void Document::emitStreamResetProgress() +{ + switch(m_streamResetState) { + case StreamResetState::None: + emit streamResetProgress(101); + break; + case StreamResetState::Generating: + emit streamResetProgress(-1); + break; + case StreamResetState::Streaming: { + qreal total = m_streamResetImageOriginalCount; + qreal sent = total - m_streamResetImage.size(); + emit streamResetProgress(qBound(0, qRound(total / sent * 100.0), 100)); + break; + } + } +} + void Document::undo() { if(!m_canvas) diff --git a/src/libclient/document.h b/src/libclient/document.h index e6faf5d20e..dc430f8dc7 100644 --- a/src/libclient/document.h +++ b/src/libclient/document.h @@ -14,6 +14,7 @@ extern "C" { #include "libclient/net/client.h" #include "libclient/net/message.h" #include +#include #include #ifdef Q_OS_ANDROID # include @@ -90,6 +91,8 @@ class Document final : public QObject, public net::Client::CommandHandler { Q_OBJECT public: + enum class StreamResetState { None, Generating, Streaming }; + explicit Document( int canvasImplementation, libclient::settings::Settings &settings, QObject *parent = nullptr); @@ -186,6 +189,11 @@ class Document final : public QObject, public net::Client::CommandHandler { bool isSessionOutOfSpace() const { return m_sessionOutOfSpace; } bool isPreparingReset() const { return m_preparingReset; } + bool isStreamingReset() const + { + return m_streamResetState != StreamResetState::None; + } + void setRecordOnConnect(const QString &filename) { m_recordOnConnect = filename; @@ -235,6 +243,7 @@ class Document final : public QObject, public net::Client::CommandHandler { void sessionResetState(const drawdance::CanvasState &canvasState); void catchupProgress(int percent); + void streamResetProgress(int percent); void canvasSaveStarted(); void canvasSaved(const QString &errorMessage); @@ -245,6 +254,9 @@ class Document final : public QObject, public net::Client::CommandHandler { void templateExported(const QString &errorMessage); void justInTimeSnapshotGenerated(); + void buildStreamResetImageFinished( + const net::MessageList &image, int messageCount, + const QString &correlator); public slots: // Convenience slots @@ -295,8 +307,15 @@ private slots: void onSessionResetted(); void onSessionOutOfSpace(); + void onLagMeasured(qint64 msec); void onSessionConfChanged(const QJsonObject &config); - void onAutoresetRequested(int maxSize, bool query); + void onAutoresetQueried(int maxSize, const QString &payload); + void onAutoresetRequested( + int maxSize, const QString &correlator, const QString &stream); + void onStreamResetStarted(const QString &correlator); + void onStreamResetProgressed(bool cancel); + void buildStreamResetImage( + const drawdance::CanvasState &canvasState, const QString &correlator); void onMoveLayerRequested( int sourceId, int targetId, bool intoGroup, bool below); @@ -339,16 +358,31 @@ private slots: void autosave(); + bool shouldRespondToAutoReset() const; void generateJustInTimeSnapshot(); void sendResetSnapshot(); + net::MessageList generateStreamSnapshot( + const drawdance::CanvasState &canvasState, + const net::MessageList &metadata, int prepended, + int &outMessageCount) const; + + void startSendingStreamResetSnapshot( + const net::MessageList &image, int messageCount, + const QString &correlator); + void sendNextStreamResetMessage(); + void setStreamResetState(StreamResetState state, int messageCount = 0); + void emitStreamResetProgress(); + QString m_currentPath; DP_SaveImageType m_currentType = DP_SAVE_IMAGE_UNKNOWN; QString m_downloadName; const int m_canvasImplementation; net::MessageList m_resetstate; + net::MessageList m_streamResetImage; net::MessageList m_messageBuffer; + QQueue m_pingHistory; canvas::CanvasModel *m_canvas; tools::ToolController *m_toolctrl; @@ -361,6 +395,7 @@ private slots: QString m_originalRecordingFilename; QString m_recordOnConnect; + QString m_autoResetCorrelator; bool m_dirty; bool m_autosave; @@ -387,6 +422,9 @@ private slots: int m_sessionResetThreshold; int m_baseResetThreshold; int m_sessionIdleTimeLimit; + int m_streamResetMessageCount = 0; + int m_streamResetImageOriginalCount = 0; + StreamResetState m_streamResetState = StreamResetState::None; bool m_sessionOutOfSpace; bool m_preparingReset; diff --git a/src/libclient/drawdance/paintengine.cpp b/src/libclient/drawdance/paintengine.cpp index 07c3117d53..ea1fb6aefd 100644 --- a/src/libclient/drawdance/paintengine.cpp +++ b/src/libclient/drawdance/paintengine.cpp @@ -22,6 +22,7 @@ PaintEngine::PaintEngine( void *rendererUser, DP_CanvasHistorySoftResetFn softResetFn, void *softResetUser, DP_PaintEnginePlaybackFn playbackFn, DP_PaintEngineDumpPlaybackFn dumpPlaybackFn, void *playbackUser, + DP_PaintEngineStreamResetStartFn streamResetStartFn, void *streamResetUser, const CanvasState &canvasState) : m_paintDc{DrawContextPool::acquire()} , m_mainDc{DrawContextPool::acquire()} @@ -33,7 +34,8 @@ PaintEngine::PaintEngine( rendererResizeFn, rendererUser, DP_snapshot_queue_on_save_point, sq.get(), softResetFn, softResetUser, wantCanvasHistoryDump, getDumpDir().toUtf8().constData(), &PaintEngine::getTimeMs, nullptr, - nullptr, playbackFn, dumpPlaybackFn, playbackUser)) + nullptr, playbackFn, dumpPlaybackFn, playbackUser, streamResetStartFn, + streamResetUser)) { } @@ -54,6 +56,7 @@ net::MessageList PaintEngine::reset( void *rendererUser, DP_CanvasHistorySoftResetFn softResetFn, void *softResetUser, DP_PaintEnginePlaybackFn playbackFn, DP_PaintEngineDumpPlaybackFn dumpPlaybackFn, void *playbackUser, + DP_PaintEngineStreamResetStartFn streamResetStartFn, void *streamResetUser, const CanvasState &canvasState, DP_Player *player) { net::MessageList localResetImage; @@ -72,7 +75,8 @@ net::MessageList PaintEngine::reset( rendererResizeFn, rendererUser, DP_snapshot_queue_on_save_point, sq.get(), softResetFn, softResetUser, wantCanvasHistoryDump, getDumpDir().toUtf8().constData(), &PaintEngine::getTimeMs, nullptr, - player, playbackFn, dumpPlaybackFn, playbackUser); + player, playbackFn, dumpPlaybackFn, playbackUser, streamResetStartFn, + streamResetUser); return localResetImage; } diff --git a/src/libclient/drawdance/paintengine.h b/src/libclient/drawdance/paintengine.h index 08f76194c6..c81115e3ce 100644 --- a/src/libclient/drawdance/paintengine.h +++ b/src/libclient/drawdance/paintengine.h @@ -38,6 +38,8 @@ class PaintEngine { DP_CanvasHistorySoftResetFn softResetFn, void *softResetUser, DP_PaintEnginePlaybackFn playbackFn, DP_PaintEngineDumpPlaybackFn dumpPlaybackFn, void *playbackUser, + DP_PaintEngineStreamResetStartFn streamResetStartFn, + void *streamResetUser, const CanvasState &canvasState = CanvasState::null()); ~PaintEngine(); @@ -57,6 +59,8 @@ class PaintEngine { DP_CanvasHistorySoftResetFn softResetFn, void *softResetUser, DP_PaintEnginePlaybackFn playbackFn, DP_PaintEngineDumpPlaybackFn dumpPlaybackFn, void *playbackUser, + DP_PaintEngineStreamResetStartFn streamResetStartFn, + void *streamResetUser, const CanvasState &canvasState = CanvasState::null(), DP_Player *player = nullptr); diff --git a/src/libclient/net/client.cpp b/src/libclient/net/client.cpp index f5535211cc..379d87259f 100644 --- a/src/libclient/net/client.cpp +++ b/src/libclient/net/client.cpp @@ -413,10 +413,19 @@ void Client::handleServerReply(const ServerReply &msg, int handledMessageIndex) case ServerReply::ReplyType::SizeLimitWarning: // No longer used since 2.1.0. Replaced by RESETREQUEST break; - case ServerReply::ReplyType::ResetRequest: - emit autoresetRequested( - reply["maxSize"].toInt(), reply["query"].toBool()); + case ServerReply::ReplyType::ResetRequest: { + int maxSize = reply[QStringLiteral("maxSize")].toInt(); + if(reply[QStringLiteral("query")].toBool()) { + emit autoresetQueried( + maxSize, reply[QStringLiteral("payload")].toString()); + } else { + emit autoresetRequested( + reply[QStringLiteral("maxSize")].toInt(), + reply[QStringLiteral("correlator")].toString(), + reply[QStringLiteral("stream")].toString()); + } break; + } case ServerReply::ReplyType::Status: emit serverStatusUpdate(reply["size"].toInt()); break; @@ -463,6 +472,12 @@ void Client::handleServerReply(const ServerReply &msg, int handledMessageIndex) case ServerReply::ReplyType::OutOfSpace: emit sessionOutOfSpace(); break; + case ServerReply::ReplyType::StreamStart: + emit streamResetStarted(reply[QStringLiteral("correlator")].toString()); + break; + case ServerReply::ReplyType::StreamProgress: + emit streamResetProgressed(reply[QStringLiteral("cancel")].toBool()); + break; } } diff --git a/src/libclient/net/client.h b/src/libclient/net/client.h index d0f82f21b2..26d27a322c 100644 --- a/src/libclient/net/client.h +++ b/src/libclient/net/client.h @@ -261,7 +261,11 @@ public slots: void bytesReceived(int); void bytesSent(int); void lagMeasured(qint64); - void autoresetRequested(int maxSize, bool query); + void autoresetQueried(int maxSize, const QString &payload); + void autoresetRequested( + int maxSize, const QString &correlator, const QString &stream); + void streamResetStarted(const QString &correlator); + void streamResetProgressed(bool cancel); void serverStatusUpdate(int historySize); void userInfoRequested(int userId); diff --git a/src/libclient/net/message.cpp b/src/libclient/net/message.cpp index 6aab92b690..76cc0b53b8 100644 --- a/src/libclient/net/message.cpp +++ b/src/libclient/net/message.cpp @@ -97,6 +97,14 @@ Message makeInternalResetMessage(uint8_t contextId) return Message::noinc(DP_msg_internal_reset_new(contextId)); } +Message makeInternalStreamResetStartMessage( + uint8_t contextId, const QString &correlator) +{ + QByteArray bytes = correlator.toUtf8(); + return Message::noinc(DP_msg_internal_stream_reset_start_new( + contextId, bytes.size(), bytes.constData())); +} + Message makeInternalSnapshotMessage(uint8_t contextId) { return Message::noinc(DP_msg_internal_snapshot_new(contextId)); @@ -313,11 +321,6 @@ makeSetMetadataIntMessage(uint8_t contextId, uint8_t field, int32_t value) return Message::noinc(DP_msg_set_metadata_int_new(contextId, field, value)); } -Message makeSoftResetMessage(uint8_t contextId) -{ - return Message::noinc(DP_msg_soft_reset_new(contextId)); -} - Message makeTrackCreateMessage( uint8_t contextId, uint16_t id, uint16_t insertId, uint16_t sourceId, const QString &title) diff --git a/src/libclient/net/message.h b/src/libclient/net/message.h index 6480fb100c..9429cfd780 100644 --- a/src/libclient/net/message.h +++ b/src/libclient/net/message.h @@ -52,6 +52,9 @@ Message makeInternalCleanupMessage(uint8_t contextId); Message makeInternalResetMessage(uint8_t contextId); +Message makeInternalStreamResetStartMessage( + uint8_t contextId, const QString &correlator); + Message makeInternalSnapshotMessage(uint8_t contextId); Message makeKeyFrameSetMessage( @@ -130,8 +133,6 @@ Message makeSelectionPutMessage( Message makeSetMetadataIntMessage(uint8_t contextId, uint8_t field, int32_t value); -Message makeSoftResetMessage(uint8_t contextId); - Message makeTrackCreateMessage( uint8_t contextId, uint16_t id, uint16_t insertId, uint16_t sourceId, const QString &title); diff --git a/src/libclient/server/builtinsession.cpp b/src/libclient/server/builtinsession.cpp index 3f1b7280ad..2bee99a4db 100644 --- a/src/libclient/server/builtinsession.cpp +++ b/src/libclient/server/builtinsession.cpp @@ -30,13 +30,15 @@ bool BuiltinSession::supportsAutoReset() const return false; } -void BuiltinSession::readyToAutoReset(int ctxId) +void BuiltinSession::readyToAutoReset( + const AutoResetResponseParams ¶ms, const QString &payload) { + Q_UNUSED(payload); log(Log() .about(Log::Level::Warn, Log::Topic::RuleBreak) .message(QStringLiteral( "User %1 sent ready-to-autoreset to builtin server!") - .arg(ctxId))); + .arg(params.ctxId))); } void BuiltinSession::doInternalReset(const drawdance::CanvasState &canvasState) @@ -65,13 +67,35 @@ void BuiltinSession::doInternalReset(const drawdance::CanvasState &canvasState) net::Message catchup = net::ServerReply::makeCatchup(msgs.size(), 1); net::Message caughtup = net::ServerReply::makeCaughtUp(1); for(Client *c : awaitingClients) { - c->setAwaitingReset(false); + c->setResetFlags(Client::ResetFlag::None); c->sendDirectMessage(catchup); c->sendDirectMessages(msgs); c->sendDirectMessage(caughtup); } } +StreamResetStartResult +BuiltinSession::handleStreamResetStart(int ctxId, const QString &correlator) +{ + Q_UNUSED(ctxId); + Q_UNUSED(correlator); + return StreamResetStartResult::Unsupported; +} + +StreamResetAbortResult BuiltinSession::handleStreamResetAbort(int ctxId) +{ + Q_UNUSED(ctxId); + return StreamResetAbortResult::Unsupported; +} + +StreamResetPrepareResult +BuiltinSession::handleStreamResetFinish(int ctxId, int expectedMessageCount) +{ + Q_UNUSED(ctxId); + Q_UNUSED(expectedMessageCount); + return StreamResetPrepareResult::Unsupported; +} + void BuiltinSession::addToHistory(const net::Message &msg) { if(state() == State::Shutdown) { @@ -123,6 +147,11 @@ void BuiltinSession::addToHistory(const net::Message &msg) } } +void BuiltinSession::onSessionInitialized() +{ + // Nothing to do. +} + void BuiltinSession::onSessionReset() { auto [msgs, lastBatchIndex] = history()->getBatch(-1); @@ -152,7 +181,7 @@ void BuiltinSession::onClientJoin(Client *client, bool host) } else { // The new client has to wait until the soft reset point is processed. // The paint engine will call us back once it has done so. - client->setAwaitingReset(true); + client->setResetFlags(Client::ResetFlag::Awaiting); if(!m_softResetRequested) { directToAll(net::makeSoftResetMessage( m_paintEngine->aclState().localUserId())); @@ -161,6 +190,27 @@ void BuiltinSession::onClientJoin(Client *client, bool host) } } +void BuiltinSession::onClientDeop(Client *client) +{ + Q_UNUSED(client); + // Nothing to do. +} + +void BuiltinSession::onResetStream(Client &client, const net::Message &msg) +{ + Q_UNUSED(msg); + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("Client %1 sent reset stream message, but " + "this is a builtin server") + .arg(client.id()))); +} + +void BuiltinSession::onStateChanged() +{ + // Nothing to do. +} + void BuiltinSession::internalReset(const drawdance::CanvasState &canvasState) { net::MessageList snapshot = serverSideStateMessages(); diff --git a/src/libclient/server/builtinsession.h b/src/libclient/server/builtinsession.h index 4e1bc0255a..261b1ebf31 100644 --- a/src/libclient/server/builtinsession.h +++ b/src/libclient/server/builtinsession.h @@ -29,17 +29,30 @@ class BuiltinSession final : public Session { BuiltinSession &operator=(BuiltinSession &&) = delete; bool supportsAutoReset() const override; - void readyToAutoReset(int ctxId) override; + void readyToAutoReset( + const AutoResetResponseParams ¶ms, const QString &payload) override; void doInternalReset(const drawdance::CanvasState &canvasState); + StreamResetStartResult + handleStreamResetStart(int ctxId, const QString &correlator) override; + + StreamResetAbortResult handleStreamResetAbort(int ctxId) override; + + StreamResetPrepareResult + handleStreamResetFinish(int ctxId, int expectedMessageCount) override; + protected: void addToHistory(const net::Message &msg) override; + void onSessionInitialized() override; void onSessionReset() override; void onClientJoin(Client *client, bool host) override; + void onClientDeop(Client *client) override; + void onResetStream(Client &client, const net::Message &msg) override; + void onStateChanged() override; private: - void internalReset(const drawdance::CanvasState &canvasState); + void internalReset(const drawdance::CanvasState &canvasState); canvas::PaintEngine *m_paintEngine; drawdance::AclState m_acls; @@ -47,7 +60,7 @@ class BuiltinSession final : public Session { size_t m_resetImageSize = 0; QString m_pinnedMessage; int m_defaultLayer = 0; - bool m_softResetRequested = false; + bool m_softResetRequested = false; }; } diff --git a/src/libserver/client.cpp b/src/libserver/client.cpp index f175f512db..19eb36b53e 100644 --- a/src/libserver/client.cpp +++ b/src/libserver/client.cpp @@ -155,9 +155,9 @@ struct Client::Private { bool isAuthenticated = false; bool isMuted = false; bool isHoldLocked = false; - bool isAwaitingReset = false; bool isBanTriggered = false; bool isGhost = false; + ResetFlags resetFlags = ResetFlag::None; BanResult ban = BanResult::notBanned(); Private(ClientSocket *socket_, ServerLog *logger_) @@ -661,14 +661,14 @@ QHostAddress Client::peerAddress() const void Client::sendDirectMessage(const net::Message &msg) { - if(!d->isAwaitingReset || msg.isControl()) { + if(!isAwaitingReset() || msg.isControl()) { d->msgqueue->send(msg); } } void Client::sendDirectMessages(const net::MessageList &msgs) { - if(d->isAwaitingReset) { + if(isAwaitingReset()) { for(const net::Message &msg : msgs) { if(msg.isControl()) { d->msgqueue->send(msg); @@ -819,14 +819,20 @@ bool Client::isHoldLocked() const return d->isHoldLocked; } -void Client::setAwaitingReset(bool awaiting) +void Client::setResetFlags(ResetFlags resetFlags) { - d->isAwaitingReset = awaiting; + d->resetFlags = resetFlags; +} + +Client::ResetFlags Client::resetFlags() const +{ + return d->resetFlags; } bool Client::isAwaitingReset() const { - return d->isAwaitingReset; + return d->resetFlags.testFlag(ResetFlag::Awaiting) && + !d->resetFlags.testFlag(ResetFlag::Streaming); } bool Client::isWebSocket() const diff --git a/src/libserver/client.h b/src/libserver/client.h index 23871ce90f..0df1710a74 100644 --- a/src/libserver/client.h +++ b/src/libserver/client.h @@ -32,8 +32,16 @@ struct BanResult; */ class Client : public QObject { Q_OBJECT - public: + enum class ResetFlag { + None = 0, + Awaiting = 1 << 0, + Queried = 1 << 1, + Responded = 1 << 2, + Streaming = 1 << 3, + }; + Q_DECLARE_FLAGS(ResetFlags, ResetFlag) + ~Client() override; //! Get the user's IP address @@ -275,13 +283,9 @@ class Client : public QObject { void setHoldLocked(bool lock); bool isHoldLocked() const; - /** - * @brief Block all messages sent to this user - * - * This state is set when a fresh reset is imminent and we don't want to - * send any messages to the client before that happens. - */ - void setAwaitingReset(bool awaiting); + void setResetFlags(ResetFlags resetFlags); + ResetFlags resetFlags() const; + bool isAwaitingReset() const; /** @@ -341,6 +345,8 @@ private slots: Private *d; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(Client::ResetFlags) + } #endif diff --git a/src/libserver/filedhistory.cpp b/src/libserver/filedhistory.cpp index cfb2a843ff..0a1af4bf74 100644 --- a/src/libserver/filedhistory.cpp +++ b/src/libserver/filedhistory.cpp @@ -58,6 +58,8 @@ FiledHistory::FiledHistory( FiledHistory::~FiledHistory() { + DP_binary_writer_free(m_resetStreamWriter); + DP_binary_reader_free(m_resetStreamReader); DP_binary_writer_free(m_writer); DP_binary_reader_free(m_reader); } @@ -136,56 +138,157 @@ bool FiledHistory::create() if(!initRecording()) return false; - if(!m_alias.isEmpty()) - m_journal->write(QString("ALIAS %1\n").arg(m_alias).toUtf8()); - m_journal->write(QString("FOUNDER %1\n").arg(m_founder).toUtf8()); - m_journal->flush(); + if(m_alias.isEmpty()) { + writeStringToJournal(QStringLiteral("FOUNDER %1\n").arg(m_founder)); + } else { + writeStringToJournal( + QStringLiteral("ALIAS %1\nFOUNDER %2\n").arg(m_alias, m_founder)); + } return true; } bool FiledHistory::initRecording() { - Q_ASSERT(m_blocks.isEmpty()); + Q_ASSERT(m_blockCache.isEmpty()); + Q_ASSERT(!m_reader); + Q_ASSERT(!m_writer); + QString fileName = uniqueRecordingFilename(m_dir, id(), ++m_fileCount); + if(!openRecording(fileName, false, &m_recording, &m_reader, &m_writer)) { + return false; + } - const QString filename = - uniqueRecordingFilename(m_dir, id(), ++m_fileCount); + writeFileEntryToJournal(fileName); + m_blockCache.addBlock(m_recording->pos(), firstIndex()); + return true; +} - m_recording = new QFile(m_dir.absoluteFilePath(filename), this); - if(!m_recording->open(QFile::ReadWrite)) { - qWarning() << filename << m_recording->errorString(); +bool FiledHistory::openRecording( + const QString &fileName, bool stream, QFile **outRecording, + DP_BinaryReader **outReader, DP_BinaryWriter **outWriter) +{ + QFile *recording = new QFile(m_dir.absoluteFilePath(fileName), this); + if(!recording->open(QFile::ReadWrite)) { + qWarning( + "Error opening '%s': %s", qUtf8Printable(fileName), + qUtf8Printable(recording->errorString())); + recording->remove(); + delete recording; return false; } - Q_ASSERT(!m_reader); - m_reader = DP_binary_reader_new( - DP_qfile_input_new(m_recording, false, DP_input_new), + DP_BinaryReader *reader = DP_binary_reader_new( + DP_qfile_input_new(recording, false, DP_input_new), DP_BINARY_READER_FLAG_NO_LENGTH | DP_BINARY_READER_FLAG_NO_HEADER); - Q_ASSERT(!m_writer); - m_writer = DP_binary_writer_new( - DP_qfile_output_new(m_recording, false, DP_output_new)); + DP_BinaryWriter *writer = DP_binary_writer_new( + DP_qfile_output_new(recording, false, DP_output_new)); - JSON_Value *header_value = json_value_init_object(); - JSON_Object *header_object = json_value_get_object(header_value); + JSON_Value *headerValue = json_value_init_object(); + JSON_Object *headerObject = json_value_get_object(headerValue); json_object_set_string( // the hosting client's protocol version - header_object, "version", qUtf8Printable(m_version.asString())); - bool ok = DP_binary_writer_write_header(m_writer, header_object); - json_value_free(header_value); + headerObject, "version", qUtf8Printable(m_version.asString())); + if(stream) { + json_object_set_number( + headerObject, "streamfork", double(m_resetStreamForkPos)); + } + bool ok = DP_binary_writer_write_header(writer, headerObject); if(!ok) { - qWarning() << filename << DP_error(); + qWarning( + "Error writing header to '%s': %s", qUtf8Printable(fileName), + DP_error()); + } + json_value_free(headerValue); + + if(ok) { + if(!recording->flush()) { + qWarning( + "Error flushing recording to '%s': %s", + qUtf8Printable(fileName), + qUtf8Printable(recording->errorString())); + ok = false; + } + } + + if(ok) { + *outRecording = recording; + *outReader = reader; + *outWriter = writer; + return true; + } else { + DP_binary_writer_free(writer); + DP_binary_reader_free(reader); + recording->remove(); + delete recording; return false; } +} + +void FiledHistory::writeFileEntryToJournal(const QString &fileName) +{ + writeStringToJournal(QStringLiteral("FILE %1\n").arg(fileName)); +} - m_recording->flush(); +void FiledHistory::writeStringToJournal(const QString &s) +{ + writeBytesToJournal(s.toUtf8()); +} - m_journal->write(QString("FILE %1\n").arg(filename).toUtf8()); - m_journal->flush(); +void FiledHistory::writeBytesToJournal(const QByteArray &bytes) +{ + if(m_journal) { + qint64 written = m_journal->write(bytes); + if(written == bytes.size()) { + flushJournal(); + } else if(written < 0) { + qWarning( + "Error writing to journal: %s", + qUtf8Printable(m_journal->errorString())); + } else { + qWarning( + "Tried to write %zu byte(s) to journal, but wrote %zu", + size_t(bytes.size()), size_t(written)); + } + } +} - m_blocks << Block{ - m_recording->pos(), firstIndex(), 0, m_recording->pos(), - net::MessageList()}; +void FiledHistory::flushRecording() +{ + if(m_recording) { + if(!m_recording->flush()) { + qWarning( + "Error flushing recording: %s", + qUtf8Printable(m_recording->errorString())); + } + } +} - return true; +void FiledHistory::flushJournal() +{ + if(m_journal) { + if(!m_journal->flush()) { + qWarning( + "Error flushing journal: %s", + qUtf8Printable(m_journal->errorString())); + } + } +} + +void FiledHistory::removeOrArchive(QFile *f) const +{ + if(f) { + if(m_archive) { + QString fileName = f->fileName(); + if(!f->rename(fileName + QStringLiteral(".archived"))) { + qWarning( + "Error archiving '%s': %s", qUtf8Printable(fileName), + qUtf8Printable(f->errorString())); + } + } else if(!f->remove()) { + qWarning( + "Error removing '%s': %s", qUtf8Printable(f->fileName()), + qUtf8Printable(f->errorString())); + } + } } bool FiledHistory::load() @@ -214,7 +317,7 @@ bool FiledHistory::load() if(cmd == "FILE") { recordingFile = QString::fromUtf8(params); ++m_fileCount; - m_blocks.clear(); + m_blockCache.clear(); } else if(cmd == "ALIAS") { if(m_alias.isEmpty()) @@ -237,7 +340,7 @@ bool FiledHistory::load() m_maxUsers = qBound(1, params.toInt(), 254); } else if(cmd == "AUTORESET") { - m_autoResetThreshold = params.toUInt(); + m_autoResetThreshold = params.toULong(); } else if(cmd == "TITLE") { m_title = params; @@ -409,9 +512,8 @@ bool FiledHistory::load() return false; } - historyLoaded( - m_blocks.last().endOffset - startOffset, - m_blocks.last().startIndex + m_blocks.last().count); + const Block &b = m_blockCache.lastBlock(); + historyLoaded(b.endOffset - startOffset, b.startIndex + b.count); // If a loaded session is empty, the server expects the first joining client // to supply the initial content, while the client is expecting to join @@ -426,37 +528,29 @@ bool FiledHistory::load() bool FiledHistory::scanBlocks() { - Q_ASSERT(m_blocks.isEmpty()); + Q_ASSERT(m_blockCache.isEmpty()); // Note: m_recording should be at the start of the recording - m_blocks << Block{ - m_recording->pos(), firstIndex(), 0, m_recording->pos(), - net::MessageList()}; + m_blockCache.addBlock(m_recording->pos(), firstIndex()); QSet users; do { - Block &b = m_blocks.last(); uint8_t msgType, ctxId; int msglen = DP_binary_reader_skip_message(m_reader, &msgType, &ctxId); if(msglen < 0) { // Truncated message encountered. // Rewind back to the end of the previous message - qWarning() << m_recording->fileName() << "Recording truncated at" - << int(b.endOffset); - m_recording->seek(b.endOffset); + qint64 offset = m_blockCache.lastBlock().endOffset; + qWarning( + "Recording '%s' truncated at %lld", + qUtf8Printable(m_recording->fileName()), + static_cast(offset)); + m_recording->seek(offset); break; } - ++m_blocks.last().count; - - b.endOffset += msglen; - Q_ASSERT(b.endOffset == m_recording->pos()); - - if(b.endOffset - b.startOffset >= MAX_BLOCK_SIZE) { - m_blocks << Block{ - b.endOffset, b.startIndex + b.count, 0, b.endOffset, - net::MessageList()}; - } + m_blockCache.incrementLastBlock(msglen); + Q_ASSERT(m_blockCache.lastBlock().endOffset == m_recording->pos()); switch(msgType) { case DP_MSG_JOIN: @@ -472,8 +566,7 @@ bool FiledHistory::scanBlocks() // There should be no users at the end of the recording. for(const uint8_t user : users) { net::Message msg = net::makeLeaveMessage(user); - m_blocks.last().count++; - m_blocks.last().endOffset += qint64(msg.length()); + m_blockCache.incrementLastBlock(msg.length()); if(DP_binary_writer_write_message(m_writer, msg.get()) == 0) { return false; } @@ -484,45 +577,30 @@ bool FiledHistory::scanBlocks() void FiledHistory::terminate() { + discardResetStream(); DP_binary_reader_free(m_reader); m_reader = nullptr; DP_binary_writer_free(m_writer); m_writer = nullptr; m_recording->close(); m_journal->close(); - - if(m_archive) { - m_journal->rename(m_journal->fileName() + ".archived"); - m_recording->rename(m_recording->fileName() + ".archived"); - } else { - m_recording->remove(); - m_journal->remove(); - } + removeOrArchive(m_journal); + removeOrArchive(m_recording); } void FiledHistory::closeBlock() { // Flush the output files just to be safe - m_recording->flush(); - m_journal->flush(); - - // Check if anything needs to be done - Block &b = m_blocks.last(); - if(b.count == 0) - return; - - // Mark last block as closed and start a new one - m_blocks << Block{ - b.endOffset, b.startIndex + b.count, 0, b.endOffset, - net::MessageList()}; + flushRecording(); + flushJournal(); + m_blockCache.closeLastBlock(); } void FiledHistory::setFounderName(const QString &founder) { if(m_founder != founder) { m_founder = founder; - m_journal->write(QString("FOUNDER %1\n").arg(m_founder).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("FOUNDER %1\n").arg(m_founder)); } } @@ -530,24 +608,17 @@ void FiledHistory::setPasswordHash(const QByteArray &password) { if(m_password != password) { m_password = password; - - m_journal->write("PASSWORD "); - if(!m_password.isEmpty()) - m_journal->write(m_password); - m_journal->write("\n"); - m_journal->flush(); + writeBytesToJournal( + QByteArrayLiteral("PASSWORD ") + m_password + + QByteArrayLiteral("\n")); } } void FiledHistory::setOpwordHash(const QByteArray &opword) { m_opword = opword; - - m_journal->write("OPWORD "); - if(!m_opword.isEmpty()) - m_journal->write(m_opword); - m_journal->write("\n"); - m_journal->flush(); + writeBytesToJournal( + QByteArrayLiteral("OPWORD ") + m_opword + QByteArrayLiteral("\n")); } void FiledHistory::setMaxUsers(int max) @@ -555,27 +626,24 @@ void FiledHistory::setMaxUsers(int max) const int newMax = qBound(1, max, 254); if(newMax != m_maxUsers) { m_maxUsers = newMax; - m_journal->write(QString("MAXUSERS %1\n").arg(newMax).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("MAXUSERS %1\n").arg(newMax)); } } -void FiledHistory::setAutoResetThreshold(uint limit) +void FiledHistory::setAutoResetThreshold(size_t limit) { - const uint newLimit = - sizeLimit() == 0 ? limit : qMin(uint(sizeLimit() * 0.9), limit); + const size_t newLimit = + sizeLimit() == 0 ? limit : qMin(size_t(sizeLimit() * 0.9), limit); if(newLimit != m_autoResetThreshold) { m_autoResetThreshold = newLimit; - m_journal->write(QString("AUTORESET %1\n").arg(newLimit).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("AUTORESET %1\n").arg(newLimit)); } } int FiledHistory::nextCatchupKey() { int result = incrementNextCatchupKey(m_nextCatchupKey); - m_journal->write(QString("CATCHUP %1\n").arg(m_nextCatchupKey).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("CATCHUP %1\n").arg(m_nextCatchupKey)); return result; } @@ -583,8 +651,7 @@ void FiledHistory::setTitle(const QString &title) { if(title != m_title) { m_title = title; - m_journal->write(QString("TITLE %1\n").arg(title).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("TITLE %1\n").arg(title)); } } @@ -614,41 +681,33 @@ void FiledHistory::setFlags(Flags f) if(f.testFlag(AllowWeb)) { fstr.append(QStringLiteral("allowweb")); } - m_journal->write( - QStringLiteral("FLAGS %1\n").arg(fstr.join(' ')).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("FLAGS %1\n").arg(fstr.join(' '))); } } void FiledHistory::joinUser(uint8_t id, const QString &name) { SessionHistory::joinUser(id, name); - m_journal->write( - "USER " + QByteArray::number(int(id)) + " " + - name.toUtf8().toPercentEncoding(QByteArray(), " ") + "\n"); - m_journal->flush(); + writeBytesToJournal( + QByteArrayLiteral("USER ") + QByteArray::number(int(id)) + + QByteArrayLiteral(" ") + + name.toUtf8().toPercentEncoding(QByteArray(), QByteArrayLiteral(" ")) + + QByteArrayLiteral("\n")); } -std::tuple FiledHistory::getBatch(int after) const +std::tuple +FiledHistory::getBatch(long long after) const { - // Find the block that contains the index *after* - int i = m_blocks.size() - 1; - for(; i > 0; --i) { - const Block &b = m_blocks.at(i - 1); - if(b.startIndex + b.count - 1 <= after) - break; - } - - Block &b = m_blocks[i]; - int idxOffset = qMax(0, after - b.startIndex + 1); + Block &b = m_blockCache.findBlock(after); + long long idxOffset = qMax(0LL, after - b.startIndex + 1LL); if(idxOffset >= b.count) { - return std::make_tuple(net::MessageList(), b.startIndex + b.count - 1); + return std::make_tuple( + net::MessageList(), b.startIndex + b.count - 1LL); } if(b.messages.isEmpty() && b.count > 0) { // Load the block worth of messages to memory if not already loaded const qint64 prevPos = m_recording->pos(); - qDebug() << m_recording->fileName() << "loading block" << i; m_recording->seek(b.startOffset); for(int m = 0; m < b.count; ++m) { DP_Message *msg; @@ -666,24 +725,13 @@ std::tuple FiledHistory::getBatch(int after) const } Q_ASSERT(b.messages.size() == b.count); return std::make_tuple( - b.messages.mid(idxOffset), b.startIndex + b.count - 1); + b.messages.mid(idxOffset), b.startIndex + b.count - 1LL); } void FiledHistory::historyAdd(const net::Message &msg) { size_t len = DP_binary_writer_write_message(m_writer, msg.get()); - - Block &b = m_blocks.last(); - b.count++; - b.endOffset += len; - - // Add message to cache, if already active (if cache is empty, it will be - // loaded from disk when needed) - if(!b.messages.isEmpty()) - b.messages.append(msg); - - if(b.endOffset - b.startOffset > MAX_BLOCK_SIZE) - closeBlock(); + m_blockCache.addToLastBlock(msg, len); } void FiledHistory::historyReset(const net::MessageList &newHistory) @@ -696,15 +744,10 @@ void FiledHistory::historyReset(const net::MessageList &newHistory) m_reader = nullptr; DP_binary_writer_free(m_writer); m_writer = nullptr; - m_blocks.clear(); + m_blockCache.clear(); initRecording(); - // Remove old recording after the new one has been created so - // that the new file will not have the same name. - if(m_archive) - oldRecording->rename(oldRecording->fileName() + ".archived"); - else - oldRecording->remove(); + removeOrArchive(oldRecording); delete oldRecording; for(const net::Message &msg : newHistory) { @@ -712,53 +755,231 @@ void FiledHistory::historyReset(const net::MessageList &newHistory) } } -void FiledHistory::cleanupBatches(int before) +void FiledHistory::cleanupBatches(long long before) { - for(Block &b : m_blocks) { - if(b.startIndex + b.count >= before) - break; - if(!b.messages.isEmpty()) { - qDebug() << "releasing history block cache from" << b.startIndex - << "to" << b.startIndex + b.count - 1; - b.messages = net::MessageList(); - } - } + m_blockCache.cleanup(before); } void FiledHistory::historyAddBan( int id, const QString &username, const QHostAddress &ip, const QString &extAuthId, const QString &sid, const QString &bannedBy) { - const QByteArray include = " "; - QByteArray entry = - "BAN " + QByteArray::number(id) + " " + - username.toUtf8().toPercentEncoding(QByteArray(), include) + " " + - ip.toString().toUtf8() + " " + - extAuthId.toUtf8().toPercentEncoding(QByteArray(), include) + " " + - bannedBy.toUtf8().toPercentEncoding(QByteArray(), include) + " " + - sid.toUtf8().toPercentEncoding(QByteArray(), include) + "\n"; - m_journal->write(entry); - m_journal->flush(); + const QByteArray space = QByteArrayLiteral(" "); + writeBytesToJournal( + QByteArrayLiteral("BAN ") + QByteArray::number(id) + space + + username.toUtf8().toPercentEncoding(QByteArray(), space) + space + + ip.toString().toUtf8() + space + + extAuthId.toUtf8().toPercentEncoding(QByteArray(), space) + space + + bannedBy.toUtf8().toPercentEncoding(QByteArray(), space) + space + + sid.toUtf8().toPercentEncoding(QByteArray(), space) + + QByteArrayLiteral("\n")); } void FiledHistory::historyRemoveBan(int id) { - m_journal->write(QByteArray("UNBAN ") + QByteArray::number(id) + "\n"); - m_journal->flush(); + writeBytesToJournal( + QByteArrayLiteral("UNBAN ") + QByteArray::number(id) + + QByteArrayLiteral("\n")); +} + +StreamResetStartResult +FiledHistory::openResetStream(const net::MessageList &serverSideStateMessages) +{ + Q_ASSERT(!m_resetStreamRecording); + Q_ASSERT(!m_resetStreamReader); + Q_ASSERT(!m_resetStreamWriter); + + m_resetStreamFileCount = ++m_fileCount; + m_resetStreamFileName = + uniqueRecordingFilename(m_dir, id(), m_resetStreamFileCount); + m_resetStreamForkPos = m_recording->pos(); + if(!openRecording( + m_resetStreamFileName, true, &m_resetStreamRecording, + &m_resetStreamReader, &m_resetStreamWriter)) { + return StreamResetStartResult::WriteError; + } + + m_resetStreamHeaderPos = m_resetStreamRecording->pos(); + m_blockCache.closeLastBlock(); + m_resetStreamBlockIndex = m_blockCache.size() - 1; + m_resetStreamBlockCache.clear(); + m_resetStreamBlockCache.addBlock(m_resetStreamHeaderPos, 0LL); + + for(const net::Message &msg : serverSideStateMessages) { + StreamResetAddResult addResult = addResetStreamMessage(msg); + if(addResult != StreamResetAddResult::Ok) { + discardResetStream(); + return addResult == StreamResetAddResult::OutOfSpace + ? StreamResetStartResult::OutOfSpace + : StreamResetStartResult::WriteError; + } + } + + return StreamResetStartResult::Ok; +} + +StreamResetAddResult +FiledHistory::addResetStreamMessage(const net::Message &msg) +{ + Q_ASSERT(m_resetStreamWriter); + size_t len = DP_binary_writer_write_message(m_resetStreamWriter, msg.get()); + if(len > 0) { + m_resetStreamBlockCache.incrementLastBlock(len); + return StreamResetAddResult::Ok; + } else { + return StreamResetAddResult::WriteError; + } +} + +StreamResetPrepareResult FiledHistory::prepareResetStream() +{ + Q_ASSERT(m_resetStreamRecording); + if(m_resetStreamRecording->flush()) { + return StreamResetPrepareResult::Ok; + } else { + qWarning( + "Error flushing stream recording: %s", + qUtf8Printable(m_resetStreamRecording->errorString())); + return StreamResetPrepareResult::WriteError; + } +} + +bool FiledHistory::resolveResetStream( + long long newFirstIndex, long long &outMessageCount, size_t &outSizeInBytes, + QString &outError) +{ + Q_ASSERT(m_resetStreamRecording); + + qint64 prevPos = m_recording->pos(); + Q_ASSERT(prevPos >= m_resetStreamForkPos); + + size_t sizeLimitInBytes = sizeLimit(); + if(sizeLimitInBytes != 0) { + size_t sizeInBytes = + (prevPos - m_resetStreamForkPos) + + (m_resetStreamRecording->pos() - m_resetStreamHeaderPos); + if(sizeInBytes > sizeLimitInBytes) { + outError = QStringLiteral("total size %1 exceeds limit %2") + .arg(sizeInBytes) + .arg(sizeLimitInBytes); + return false; + } + } + + if(!copyForkMessagesToResetStream(outError)) { + if(!m_recording->seek(prevPos)) { + qWarning( + "Error seeking back recording: %s", + qUtf8Printable(m_recording->errorString())); + } + discardResetStream(); + return false; + } + + writeFileEntryToJournal(m_resetStreamFileName); + + DP_binary_reader_free(m_reader); + m_reader = m_resetStreamReader; + m_resetStreamReader = nullptr; + + DP_binary_writer_free(m_writer); + m_writer = m_resetStreamWriter; + m_resetStreamWriter = nullptr; + + removeOrArchive(m_recording); + delete m_recording; + m_recording = m_resetStreamRecording; + m_resetStreamRecording = nullptr; + + m_blockCache.replaceWithResetStream( + m_resetStreamBlockCache, m_resetStreamBlockIndex, newFirstIndex); + + outMessageCount = m_blockCache.totalMessageCount(); + outSizeInBytes = m_recording->pos() - m_resetStreamHeaderPos; + return true; +} + +bool FiledHistory::copyForkMessagesToResetStream(QString &outError) +{ + if(!m_recording->seek(m_resetStreamForkPos)) { + outError = QStringLiteral("Error seeking recording to stream start: %1") + .arg(m_recording->errorString()); + return false; + } + + QByteArray buffer; + buffer.resize(BUFSIZ); + while(true) { + qint64 read = m_recording->read(buffer.data(), buffer.size()); + if(read > 0) { + qint64 written = + m_resetStreamRecording->write(buffer.constData(), read); + if(written < 0) { + outError = + QStringLiteral("Error writing to stream recording: %1") + .arg(m_resetStreamRecording->errorString()); + return false; + } else if(written != read) { + outError = + QStringLiteral("Tried to write %1 byte(s), but wrote %2") + .arg(read) + .arg(written); + return false; + } + } else if(read == 0) { + break; + } else { + outError = QStringLiteral("Error reading from recording: %1") + .arg(m_recording->errorString()); + return false; + } + } + + if(!m_resetStreamRecording->flush()) { + outError = QStringLiteral("Error flushing stream recording: %1") + .arg(m_resetStreamRecording->errorString()); + return false; + } + + return true; +} + +void FiledHistory::discardResetStream() +{ + if(m_resetStreamFileCount == m_fileCount) { + --m_fileCount; + } + m_resetStreamFileCount = -1; + m_resetStreamBlockCache.clear(); + + if(m_resetStreamRecording) { + DP_ASSERT(m_resetStreamWriter); + DP_binary_writer_free(m_resetStreamWriter); + m_resetStreamWriter = nullptr; + + DP_ASSERT(m_resetStreamReader); + DP_binary_reader_free(m_resetStreamReader); + m_resetStreamReader = nullptr; + + m_resetStreamRecording->remove(); + delete m_resetStreamRecording; + m_resetStreamRecording = nullptr; + } else { + DP_ASSERT(!m_resetStreamWriter); + DP_ASSERT(!m_resetStreamReader); + } } void FiledHistory::timerEvent(QTimerEvent *) { - if(m_recording) - m_recording->flush(); + flushRecording(); } void FiledHistory::addAnnouncement(const QString &url) { if(!m_announcements.contains(url)) { m_announcements << url; - m_journal->write(QString("ANNOUNCE %1\n").arg(url).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("ANNOUNCE %1\n").arg(url)); } } @@ -766,8 +987,7 @@ void FiledHistory::removeAnnouncement(const QString &url) { if(m_announcements.contains(url)) { m_announcements.removeAll(url); - m_journal->write(QString("UNANNOUNCE %1\n").arg(url).toUtf8()); - m_journal->flush(); + writeStringToJournal(QStringLiteral("UNANNOUNCE %1\n").arg(url)); } } @@ -775,11 +995,9 @@ void FiledHistory::setAuthenticatedOperator(const QString &authId, bool op) { bool currentlyOp = isOperator(authId); if(op && !currentlyOp) { - m_journal->write("OP " + authId.toUtf8() + "\n"); - m_journal->flush(); + writeStringToJournal(QStringLiteral("OP %1\n").arg(authId)); } else if(!op && currentlyOp) { - m_journal->write("DEOP " + authId.toUtf8() + "\n"); - m_journal->flush(); + writeStringToJournal(QStringLiteral("DEOP %1\n").arg(authId)); } SessionHistory::setAuthenticatedOperator(authId, op); } @@ -788,11 +1006,9 @@ void FiledHistory::setAuthenticatedTrust(const QString &authId, bool trusted) { bool currentlyTrusted = isTrusted(authId); if(trusted && !currentlyTrusted) { - m_journal->write("TRUST " + authId.toUtf8() + "\n"); - m_journal->flush(); + writeStringToJournal(QStringLiteral("TRUST %1\n").arg(authId)); } else if(!trusted && currentlyTrusted) { - m_journal->write("UNTRUST " + authId.toUtf8() + "\n"); - m_journal->flush(); + writeStringToJournal(QStringLiteral("UNTRUST %1\n").arg(authId)); } SessionHistory::setAuthenticatedTrust(authId, trusted); } @@ -802,12 +1018,121 @@ void FiledHistory::setAuthenticatedUsername( { const QString *currentUsername = authenticatedUsernameFor(authId); if(!currentUsername || *currentUsername != username) { - m_journal->write( - "AUTHNAME " + authId.toUtf8().toPercentEncoding() + " " + - username.toUtf8().toPercentEncoding() + "\n"); - m_journal->flush(); + writeBytesToJournal( + QByteArrayLiteral("AUTHNAME ") + + authId.toUtf8().toPercentEncoding() + QByteArrayLiteral(" ") + + username.toUtf8().toPercentEncoding() + QByteArrayLiteral("\n")); } SessionHistory::setAuthenticatedUsername(authId, username); } + +FiledHistory::Block &FiledHistory::BlockCache::findBlock(long long after) +{ + int i = m_blocks.size() - 1; + for(; i > 0; --i) { + const Block &b = m_blocks.at(i - 1); + if(b.startIndex + b.count - 1LL <= after) { + break; + } + } + return m_blocks[i]; +} + +void FiledHistory::BlockCache::addBlock(qint64 offset, long long index) +{ + m_blocks.append(Block(offset, index)); +} + +void FiledHistory::BlockCache::addToLastBlock( + const net::Message &msg, size_t len) +{ + Block &b = m_blocks.last(); + // Add message to cache, if already active (if cache is empty, it will be + // loaded from disk when needed) + if(!b.messages.isEmpty()) { + b.messages.append(msg); + } + incrementBlock(b, len); +} + +void FiledHistory::BlockCache::incrementLastBlock(size_t len) +{ + Block &b = m_blocks.last(); + Q_ASSERT(b.messages.isEmpty()); + incrementBlock(b, len); +} + +void FiledHistory::BlockCache::closeLastBlock() +{ + // Check if anything needs to be done + Block &b = m_blocks.last(); + if(b.count != 0) { + // Mark last block as closed and start a new one + m_blocks.append(Block(b.endOffset, b.startIndex + b.count)); + } +} + +void FiledHistory::BlockCache::cleanup(long long before) +{ + for(Block &b : m_blocks) { + if(b.startIndex + b.count >= before) { + break; + } else if(!b.messages.isEmpty()) { + qDebug( + "Releasing history block cache from %lld to %lld", b.startIndex, + b.startIndex + b.count - 1LL); + b.messages = net::MessageList(); + } + } +} + +long long FiledHistory::BlockCache::totalMessageCount() const +{ + compat::sizetype count = m_blocks.size(); + if(count > 0) { + const Block &b = m_blocks[count - 1]; + return b.startIndex + b.count - m_blocks[0].startIndex; + } else { + return 0LL; + } +} + +void FiledHistory::BlockCache::replaceWithResetStream( + BlockCache &streamCache, compat::sizetype blockIndex, + long long newFirstIndex) +{ + // Reindex blocks in the stream reset image based on the new first index. + long long nextStartIndex = newFirstIndex; + for(Block &b : streamCache.m_blocks) { + b.startIndex = nextStartIndex; + nextStartIndex = b.startIndex + b.count; + } + + // Move forked blocks over to the new stream cache. + compat::sizetype count = m_blocks.size(); + for(compat::sizetype i = blockIndex; i < count; ++i) { + Block b = m_blocks[i]; + b.startIndex = nextStartIndex; + nextStartIndex = b.startIndex + b.count; + qint64 offsetSize = b.endOffset - b.startOffset; + b.startOffset = streamCache.m_blocks.last().endOffset; + b.endOffset = b.startOffset + offsetSize; + streamCache.m_blocks.append(b); + } + + // Replace ourselves. + m_blocks.clear(); + m_blocks.swap(streamCache.m_blocks); +} + +void FiledHistory::BlockCache::incrementBlock(Block &b, size_t len) +{ + ++b.count; + b.endOffset += len; + if(b.endOffset - b.startOffset > MAX_BLOCK_SIZE) { + m_blocks.append(Block(b.endOffset, b.startIndex + b.count)); + } +} + } diff --git a/src/libserver/filedhistory.h b/src/libserver/filedhistory.h index 81086921cf..8fef7ff59d 100644 --- a/src/libserver/filedhistory.h +++ b/src/libserver/filedhistory.h @@ -3,6 +3,7 @@ #define DP_SERVER_FILEDHISTORY_H #include "libserver/sessionhistory.h" #include "libshared/net/protover.h" +#include "libshared/util/qtcompat.h" #include #include @@ -67,7 +68,7 @@ class FiledHistory final : public SessionHistory { QByteArray opwordHash() const override { return m_opword; } int maxUsers() const override { return m_maxUsers; } QString title() const override { return m_title; } - uint autoResetThreshold() const override { return m_autoResetThreshold; } + size_t autoResetThreshold() const override { return m_autoResetThreshold; } Flags flags() const override { return m_flags; } void setFounderName(const QString &founder) override; @@ -76,13 +77,14 @@ class FiledHistory final : public SessionHistory { void setMaxUsers(int max) override; void setTitle(const QString &title) override; void setFlags(Flags f) override; - void setAutoResetThreshold(uint limit) override; + void setAutoResetThreshold(size_t limit) override; int nextCatchupKey() override; void joinUser(uint8_t id, const QString &name) override; void terminate() override; - void cleanupBatches(int before) override; - std::tuple getBatch(int after) const override; + void cleanupBatches(long long before) override; + std::tuple + getBatch(long long after) const override; void addAnnouncement(const QString &) override; void removeAnnouncement(const QString &url) override; @@ -104,30 +106,86 @@ class FiledHistory final : public SessionHistory { const QString &bannedBy) override; void historyRemoveBan(int id) override; + StreamResetStartResult + openResetStream(const net::MessageList &serverSideStateMessages) override; + StreamResetAddResult + addResetStreamMessage(const net::Message &msg) override; + StreamResetPrepareResult prepareResetStream() override; + bool resolveResetStream( + long long newFirstIndex, long long &outMessageCount, + size_t &outSizeInBytes, QString &outError) override; + void discardResetStream() override; + void timerEvent(QTimerEvent *event) override; private: - FiledHistory( - const QDir &dir, QFile *journal, const QString &id, - const QString &alias, const protocol::ProtocolVersion &version, - const QString &founder, QObject *parent); - FiledHistory( - const QDir &dir, QFile *journal, const QString &id, QObject *parent); - struct Block { qint64 startOffset; - int startIndex; - int count; + long long startIndex; + long long count; qint64 endOffset; net::MessageList messages; + + Block(qint64 offset, long long index) + : startOffset(offset) + , startIndex(index) + , count(0LL) + , endOffset(offset) + { + } }; - void discardWriterOnError(const QString &context, const QString &filename); + class BlockCache { + public: + const Block &lastBlock() const { return m_blocks.last(); } + Block &findBlock(long long after); + + void addBlock(qint64 offset, long long index); + void addToLastBlock(const net::Message &msg, size_t len); + void incrementLastBlock(size_t len); + void closeLastBlock(); + + void cleanup(long long before); + + long long totalMessageCount() const; + + compat::sizetype size() const { return m_blocks.size(); } + void clear() { m_blocks.clear(); } + bool isEmpty() const { return m_blocks.isEmpty(); } + + void replaceWithResetStream( + BlockCache &streamCache, compat::sizetype blockIndex, + long long newFirstIndex); + + private: + void incrementBlock(Block &b, size_t len); + + QVector m_blocks; + }; + + FiledHistory( + const QDir &dir, QFile *journal, const QString &id, + const QString &alias, const protocol::ProtocolVersion &version, + const QString &founder, QObject *parent); + FiledHistory( + const QDir &dir, QFile *journal, const QString &id, QObject *parent); bool create(); bool load(); bool scanBlocks(); bool initRecording(); + bool openRecording( + const QString &fileName, bool stream, QFile **outRecording, + DP_BinaryReader **outReader, DP_BinaryWriter **outWriter); + + void writeFileEntryToJournal(const QString &fileName); + void writeStringToJournal(const QString &s); + void writeBytesToJournal(const QByteArray &bytes); + void flushRecording(); + void flushJournal(); + void removeOrArchive(QFile *recording) const; + + bool copyForkMessagesToResetStream(QString &outError); QDir m_dir; QFile *m_journal; @@ -143,14 +201,24 @@ class FiledHistory final : public SessionHistory { QByteArray m_password; QByteArray m_opword; int m_maxUsers; - uint m_autoResetThreshold; + size_t m_autoResetThreshold; Flags m_flags; int m_nextCatchupKey; QStringList m_announcements; - mutable QVector m_blocks; + mutable BlockCache m_blockCache; int m_fileCount; bool m_archive; + + QString m_resetStreamFileName; + QFile *m_resetStreamRecording = nullptr; + DP_BinaryReader *m_resetStreamReader = nullptr; + DP_BinaryWriter *m_resetStreamWriter = nullptr; + int m_resetStreamFileCount = -1; + qint64 m_resetStreamForkPos; + qint64 m_resetStreamHeaderPos; + compat::sizetype m_resetStreamBlockIndex; + BlockCache m_resetStreamBlockCache; }; } diff --git a/src/libserver/inmemoryhistory.cpp b/src/libserver/inmemoryhistory.cpp index 3ff4165348..2ec5b36838 100644 --- a/src/libserver/inmemoryhistory.cpp +++ b/src/libserver/inmemoryhistory.cpp @@ -24,12 +24,13 @@ int InMemoryHistory::nextCatchupKey() return incrementNextCatchupKey(m_nextCatchupKey); } -std::tuple InMemoryHistory::getBatch(int after) const +std::tuple +InMemoryHistory::getBatch(long long after) const { if(after >= lastIndex()) return std::make_tuple(net::MessageList(), lastIndex()); - const int offset = qMax(0, after - firstIndex() + 1); + const long long offset = qMax(0LL, after - firstIndex() + 1LL); Q_ASSERT(offset < m_history.size()); return std::make_tuple(m_history.mid(offset), lastIndex()); @@ -42,7 +43,70 @@ void InMemoryHistory::historyAdd(const net::Message &msg) void InMemoryHistory::historyReset(const net::MessageList &newHistory) { + Q_ASSERT(m_resetStream.isEmpty()); m_history = newHistory; } +StreamResetStartResult InMemoryHistory::openResetStream( + const net::MessageList &serverSideStateMessages) +{ + m_resetStream = serverSideStateMessages; + m_resetStreamIndex = m_history.size(); + return StreamResetStartResult::Ok; +} + +StreamResetAddResult +InMemoryHistory::addResetStreamMessage(const net::Message &msg) +{ + m_resetStream.append(msg); + return StreamResetAddResult::Ok; +} + +StreamResetPrepareResult InMemoryHistory::prepareResetStream() +{ + // Nothing to do here. + return StreamResetPrepareResult::Ok; +} + +bool InMemoryHistory::resolveResetStream( + long long newFirstIndex, long long &outMessageCount, size_t &outSizeInBytes, + QString &outError) +{ + Q_UNUSED(newFirstIndex); + + int end = m_history.size(); + Q_ASSERT(m_resetStreamIndex <= end); + + size_t sizeInBytes = 0; + for(const net::Message &msg : m_resetStream) { + sizeInBytes += msg.length(); + } + for(int i = m_resetStreamIndex; i < end; ++i) { + sizeInBytes += m_history[i].length(); + } + outSizeInBytes = sizeInBytes; + + size_t sizeLimitInBytes = sizeLimit(); + if(sizeLimitInBytes == 0 || sizeInBytes <= sizeLimitInBytes) { + for(int i = m_resetStreamIndex; i < end; ++i) { + m_resetStream.append(m_history[i]); + } + m_history.clear(); + m_history.swap(m_resetStream); + outMessageCount = m_history.size(); + return true; + } else { + outError = QStringLiteral("total size %1 exceeds limit %2") + .arg(sizeInBytes) + .arg(sizeLimitInBytes); + return false; + } +} + +void InMemoryHistory::discardResetStream() +{ + m_resetStream.clear(); + m_resetStreamIndex = -1; +} + } diff --git a/src/libserver/inmemoryhistory.h b/src/libserver/inmemoryhistory.h index ba29b7ff52..760d2b8928 100644 --- a/src/libserver/inmemoryhistory.h +++ b/src/libserver/inmemoryhistory.h @@ -18,14 +18,15 @@ class InMemoryHistory final : public SessionHistory { const protocol::ProtocolVersion &version, const QString &founder, QObject *parent = nullptr); - std::tuple getBatch(int after) const override; + std::tuple + getBatch(long long after) const override; void terminate() override { // nothing to do } - void cleanupBatches(int) override + void cleanupBatches(long long) override { // no caching, nothing to do } @@ -53,14 +54,14 @@ class InMemoryHistory final : public SessionHistory { void setTitle(const QString &title) override { m_title = title; } Flags flags() const override { return m_flags; } void setFlags(Flags f) override { m_flags = f; } - void setAutoResetThreshold(uint limit) override + void setAutoResetThreshold(size_t limit) override { if(sizeLimit() == 0) m_autoReset = limit; else - m_autoReset = qMin(uint(sizeLimit() * 0.9), limit); + m_autoReset = qMin(size_t(sizeLimit() * 0.9), limit); } - uint autoResetThreshold() const override { return m_autoReset; } + size_t autoResetThreshold() const override { return m_autoReset; } int nextCatchupKey() override; void addAnnouncement(const QString &url) override @@ -84,9 +85,17 @@ class InMemoryHistory final : public SessionHistory { const QString &, const QString &) override { /* not persistent */ } - void historyRemoveBan(int) override - { /* not persistent */ - } + void historyRemoveBan(int) override { /* not persistent */ } + + StreamResetStartResult + openResetStream(const net::MessageList &serverSideStateMessages) override; + StreamResetAddResult + addResetStreamMessage(const net::Message &msg) override; + StreamResetPrepareResult prepareResetStream() override; + bool resolveResetStream( + long long newFirstIndex, long long &outMessageCount, + size_t &outSizeInBytes, QString &outError) override; + void discardResetStream() override; private: net::MessageList m_history; @@ -98,9 +107,11 @@ class InMemoryHistory final : public SessionHistory { QByteArray m_password; QByteArray m_opword; int m_maxUsers; - uint m_autoReset; + size_t m_autoReset; Flags m_flags; int m_nextCatchupKey; + int m_resetStreamIndex = -1; + net::MessageList m_resetStream; }; } diff --git a/src/libserver/loginhandler.cpp b/src/libserver/loginhandler.cpp index b5ba72499e..083f7f620d 100644 --- a/src/libserver/loginhandler.cpp +++ b/src/libserver/loginhandler.cpp @@ -6,6 +6,7 @@ #include "libserver/serverlog.h" #include "libserver/session.h" #include "libserver/sessions.h" +#include "libshared/net/protover.h" #include "libshared/net/servercmd.h" #include "libshared/util/authtoken.h" #include "libshared/util/networkaccess.h" diff --git a/src/libserver/opcommands.cpp b/src/libserver/opcommands.cpp index 57491768cb..82c28820b1 100644 --- a/src/libserver/opcommands.cpp +++ b/src/libserver/opcommands.cpp @@ -108,8 +108,39 @@ CmdResult readyToAutoReset( Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); - Q_UNUSED(kwargs); - client->session()->readyToAutoReset(client->id()); + + Session::ResetCapabilities capabilities; + for(const QJsonValue &capability : + kwargs[QStringLiteral("capabilities")].toArray()) { + if(capability == QStringLiteral("gzip1")) { + capabilities.setFlag(Session::ResetCapability::GzipStream); + } + } + + qreal averagePing = 0.0; + QJsonArray pingValues = kwargs[QStringLiteral("pings")].toArray(); + if(!pingValues.isEmpty()) { + qreal totalPing = 0.0; + qreal pingCount = 0.0; + for(const QJsonValue &pingValue : pingValues) { + qreal ping = pingValue.toDouble(); + if(ping > 0.0) { + totalPing += ping; + pingCount += 1.0; + } + } + if(pingCount > 0.0) { + averagePing = totalPing / pingCount; + } + } + + client->session()->readyToAutoReset( + Session::AutoResetResponseParams{ + client->id(), capabilities, + net::ServerCommand::rateAutoresetOs( + kwargs[QStringLiteral("os")].toString()), + kwargs[QStringLiteral("net")].toDouble(), averagePing}, + kwargs[QStringLiteral("payload")].toString()); return CmdResult::ok(); } @@ -711,6 +742,100 @@ authList(Client *client, const QJsonArray &args, const QJsonObject &kwargs) return CmdResult::ok(); } +CmdResult streamResetStart( + Client *client, const QJsonArray &args, const QJsonObject &kwargs) +{ + Q_UNUSED(kwargs); + if(args.size() != 1) { + return CmdResult::err(QStringLiteral( + "Stream reset start expected one argument: correlator")); + } + + Session *session = client->session(); + StreamResetStartResult result = + session->handleStreamResetStart(client->id(), args[0].toString()); + switch(result) { + case StreamResetStartResult::Ok: + return CmdResult::ok(); + case StreamResetStartResult::Unsupported: + return CmdResult::err(QStringLiteral("not supported by this session")); + case StreamResetStartResult::InvalidSessionState: + return CmdResult::err(QStringLiteral("invalid session state")); + case StreamResetStartResult::InvalidCorrelator: + return CmdResult::err(QStringLiteral("invalid correlator")); + case StreamResetStartResult::InvalidUser: + return CmdResult::err(QStringLiteral("invalid user")); + case StreamResetStartResult::AlreadyActive: + return CmdResult::err(QStringLiteral("already started")); + case StreamResetStartResult::OutOfSpace: + return CmdResult::err(QStringLiteral("out of space")); + case StreamResetStartResult::WriteError: + return CmdResult::err(QStringLiteral("write error")); + } + return CmdResult::err(QStringLiteral("unknown error %d").arg(int(result))); +} + +CmdResult streamResetAbort( + Client *client, const QJsonArray &args, const QJsonObject &kwargs) +{ + Q_UNUSED(kwargs); + if(args.size() != 0) { + return CmdResult::err( + QStringLiteral("Stream reset abort expected no arguments")); + } + + Session *session = client->session(); + StreamResetAbortResult result = + session->handleStreamResetAbort(client->id()); + switch(result) { + case StreamResetAbortResult::Ok: + return CmdResult::ok(); + case StreamResetAbortResult::Unsupported: + return CmdResult::err(QStringLiteral("not supported by this session")); + case StreamResetAbortResult::InvalidUser: + return CmdResult::err(QStringLiteral("invalid user")); + case StreamResetAbortResult::NotActive: + return CmdResult::err(QStringLiteral("no stream reset active")); + } + return CmdResult::err(QStringLiteral("unknown error %d").arg(int(result))); +} + +CmdResult streamResetFinish( + Client *client, const QJsonArray &args, const QJsonObject &kwargs) +{ + Q_UNUSED(kwargs); + if(args.size() != 1) { + return CmdResult::err(QStringLiteral( + "Stream reset finish expected one argument: message count")); + } + + Session *session = client->session(); + StreamResetPrepareResult result = + session->handleStreamResetFinish(client->id(), args[0].toInt()); + switch(result) { + case server::StreamResetPrepareResult::Ok: + return CmdResult::ok(); + case server::StreamResetPrepareResult::Unsupported: + return CmdResult::err(QStringLiteral("not supported by this session")); + case StreamResetPrepareResult::InvalidUser: + return CmdResult::err(QStringLiteral("invalid user")); + case StreamResetPrepareResult::InvalidMessageCount: + return CmdResult::err(QStringLiteral("invalid message count")); + case server::StreamResetPrepareResult::NotActive: + return CmdResult::err(QStringLiteral("no stream reset active")); + case StreamResetPrepareResult::OutOfSpace: + return CmdResult::err(QStringLiteral("reset image too large")); + break; + case StreamResetPrepareResult::WriteError: + return CmdResult::err(QStringLiteral("write error")); + break; + case StreamResetPrepareResult::ConsumerError: + return CmdResult::err(QStringLiteral("stream consumer error")); + break; + } + return CmdResult::err(QStringLiteral("unknown error %d").arg(int(result))); +} + SrvCommandSet::SrvCommandSet() { commands << SrvCommand("ready-to-autoreset", readyToAutoReset) @@ -729,7 +854,10 @@ SrvCommandSet::SrvCommandSet() << SrvCommand("report", reportAbuse, SrvCommand::NONOP) << SrvCommand("export-bans", exportBans) << SrvCommand("import-bans", importBans) - << SrvCommand("auth-list", authList); + << SrvCommand("auth-list", authList) + << SrvCommand("stream-reset-start", streamResetStart) + << SrvCommand("stream-reset-abort", streamResetAbort) + << SrvCommand("stream-reset-finish", streamResetFinish); } } // end of anonymous namespace diff --git a/src/libserver/session.cpp b/src/libserver/session.cpp index 57cc49b791..842c8bf11d 100644 --- a/src/libserver/session.cpp +++ b/src/libserver/session.cpp @@ -127,7 +127,16 @@ void Session::switchState(State newstate) m_initUser = -1; bool success = true; - if(m_state == State::Reset && !m_resetstream.isEmpty()) { + if(m_state == State::Initialization) { + m_history->resetAutoResetThresholdBase(); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message(QStringLiteral("Session initialized with size %1") + .arg(QLocale::c().formattedDataSize( + m_history->sizeInBytes())))); + onSessionInitialized(); + + } else if(m_state == State::Reset && !m_resetstream.isEmpty()) { // Reset buffer uploaded. Now perform the reset before returning to // normal running state. @@ -175,6 +184,7 @@ void Session::switchState(State newstate) } m_state = newstate; + onStateChanged(); } void Session::assignId(Client *user) @@ -307,11 +317,7 @@ void Session::removeUser(Client *user) disconnect(user, nullptr, this, nullptr); disconnect(m_history, nullptr, user, nullptr); - if(user->id() == m_initUser && m_state == State::Reset) { - // Whoops, the resetter left before the job was done! - // We simply cancel the reset in that case and go on - abortReset(); - } + onClientLeave(user); if(!isGhost) { addToHistory(net::makeLeaveMessage(user->id())); @@ -329,6 +335,15 @@ void Session::removeUser(Client *user) emit sessionAttributeChanged(this); } +void Session::onClientLeave(Client *client) +{ + if(client->id() == m_initUser && m_state == State::Reset) { + // Whoops, the resetter left before the job was done! + // We simply cancel the reset in that case and go on + abortReset(); + } +} + void Session::abortReset() { m_initUser = -1; @@ -617,14 +632,17 @@ QVector Session::updateOwnership( const bool op = ids.contains(c->id()) || c->isModerator(); if(op != c->isOperator()) { needsUpdate = true; - if(!op && c->id() == m_initUser && m_state == State::Reset) { - // OP status removed mid-reset! The user probably has at least - // part of the reset image still queued for upload, which will - // messs up the session once we're out of reset mode. Kicking - // the client is the easiest workaround. - // TODO for 3.0: send a cancel command to the client and ignore - // all further input until ack is received. - kickResetter = c; + if(!op) { + onClientDeop(c); + if(c->id() == m_initUser && m_state == State::Reset) { + // OP status removed mid-reset! The user probably has at + // least part of the reset image still queued for upload, + // which will messs up the session once we're out of reset + // mode. Kicking the client is the easiest workaround. + // TODO for 3.0: send a cancel command to the client and + // ignore all further input until ack is received. + kickResetter = c; + } } c->setOperator(op); @@ -1009,6 +1027,9 @@ void Session::handleClientMessage(Client &client, const net::Message &msg) msg.contextId(), updateTrustedUsers(ids, client.username()))); return; } + case DP_MSG_RESET_STREAM: + onResetStream(client, msg); + return; default: break; } diff --git a/src/libserver/session.h b/src/libserver/session.h index 186bfd3a30..7f39f18649 100644 --- a/src/libserver/session.h +++ b/src/libserver/session.h @@ -5,7 +5,6 @@ #include "libserver/jsonapi.h" #include "libserver/sessionhistory.h" #include "libshared/net/message.h" -#include "libshared/net/protover.h" #include #include #include @@ -39,6 +38,9 @@ class Session : public QObject, public sessionlisting::Announcable { //! State of the session enum class State { Initialization, Running, Reset, Shutdown }; + enum class ResetCapability { GzipStream = 1 << 0 }; + Q_DECLARE_FLAGS(ResetCapabilities, ResetCapability) + //! Information about a user who has since logged out struct PastClient { int id; @@ -49,6 +51,14 @@ class Session : public QObject, public sessionlisting::Announcable { bool isBannable; }; + struct AutoResetResponseParams { + int ctxId; + ResetCapabilities capabilities; + int osQuality; + qreal netQuality; + qreal averagePing; + }; + ~Session() override; //! Get the server configuration @@ -269,11 +279,20 @@ class Session : public QObject, public sessionlisting::Announcable { // Resetting related functions, called via opcommands void resetSession(int resetter); - virtual void readyToAutoReset(int ctxId) = 0; + virtual void readyToAutoReset( + const AutoResetResponseParams ¶ms, const QString &payload) = 0; void handleInitBegin(int ctxId); void handleInitComplete(int ctxId); void handleInitCancel(int ctxId); + virtual StreamResetStartResult + handleStreamResetStart(int ctxId, const QString &correlator) = 0; + + virtual StreamResetAbortResult handleStreamResetAbort(int ctxId) = 0; + + virtual StreamResetPrepareResult + handleStreamResetFinish(int ctxId, int expectedMessageCount) = 0; + /** * @brief Grant or revoke OP status of a user * @param id user ID @@ -391,17 +410,31 @@ private slots: //! Add a message to the session history virtual void addToHistory(const net::Message &msg) = 0; + //! Session history was just initialized after hosting + virtual void onSessionInitialized() = 0; + //! Session history was just reset virtual void onSessionReset() = 0; //! A regular (non-hosting) client just joined virtual void onClientJoin(Client *client, bool host) = 0; + //! A client just left, clean up reset states and similar + virtual void onClientLeave(Client *client); + + //! Client has just been deopped, cancel streamed resets and such + virtual void onClientDeop(Client *client) = 0; + + //! A streamed reset message was received + virtual void onResetStream(Client &client, const net::Message &msg) = 0; + //! This message was just added to session history void addedToHistory(const net::Message &msg); void switchState(State newstate); + virtual void onStateChanged() = 0; + //! Get the user join, SessionOwner, etc. messages that should be prepended //! to a reset image net::MessageList serverSideStateMessages() const; @@ -483,6 +516,8 @@ namespace diagnostic_marker_private { class [[maybe_unused]] AbstractSessionMarker : Session {}; } +Q_DECLARE_OPERATORS_FOR_FLAGS(Session::ResetCapabilities) + } #endif diff --git a/src/libserver/sessionhistory.cpp b/src/libserver/sessionhistory.cpp index 2bba04b274..259304ea39 100644 --- a/src/libserver/sessionhistory.cpp +++ b/src/libserver/sessionhistory.cpp @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "libserver/sessionhistory.h" +extern "C" { +#include +} #include "libserver/client.h" +#include "libserver/sessionhistory.h" +#include "libshared/net/servercmd.h" namespace server { @@ -16,7 +20,7 @@ SessionHistory::SessionHistory(const QString &id, QObject *parent) { } -bool SessionHistory::hasSpaceFor(uint bytes, uint extra) const +bool SessionHistory::hasSpaceFor(size_t bytes, size_t extra) const { return m_sizeLimit <= 0 || m_sizeInBytes + bytes <= m_sizeLimit + extra; } @@ -73,7 +77,7 @@ void SessionHistory::joinUser(uint8_t id, const QString &name) idQueue().setIdForName(id, name); } -void SessionHistory::historyLoaded(uint size, int messageCount) +void SessionHistory::historyLoaded(size_t size, int messageCount) { Q_ASSERT(m_lastIndex == -1); m_sizeInBytes = size; @@ -83,7 +87,7 @@ void SessionHistory::historyLoaded(uint size, int messageCount) bool SessionHistory::addMessage(const net::Message &msg) { - uint bytes = uint(msg.length()); + size_t bytes = msg.length(); if(hasRegularSpaceFor(bytes)) { addMessageInternal(msg, bytes); return true; @@ -103,7 +107,7 @@ bool SessionHistory::addEmergencyMessage(const net::Message &msg) } } -void SessionHistory::addMessageInternal(const net::Message &msg, uint bytes) +void SessionHistory::addMessageInternal(const net::Message &msg, size_t bytes) { m_sizeInBytes += bytes; ++m_lastIndex; @@ -113,36 +117,235 @@ void SessionHistory::addMessageInternal(const net::Message &msg, uint bytes) bool SessionHistory::reset(const net::MessageList &newHistory) { - uint newSize = 0; + size_t newSize = 0; for(const net::Message &msg : newHistory) { - newSize += uint(msg.length()); + newSize += msg.length(); } if(m_sizeLimit > 0 && newSize > m_sizeLimit) { return false; } + abortStreamedReset(); m_sizeInBytes = newSize; - m_firstIndex = m_lastIndex + 1; + m_firstIndex = m_lastIndex + 1LL; m_lastIndex += newHistory.size(); - m_autoResetBaseSize = newSize; + resetAutoResetThresholdBase(); historyReset(newHistory); emit newMessagesAvailable(); return true; } -uint SessionHistory::effectiveAutoResetThreshold() const +StreamResetStartResult SessionHistory::startStreamedReset( + uint8_t ctxId, const QString &correlator, + const net::MessageList &serverSideStateMessages) +{ + if(m_resetStreamState != ResetStreamState::None) { + return StreamResetStartResult::AlreadyActive; + } + + if(!addMessage(net::makeSoftResetMessage(0)) || + !addMessage( + net::ServerReply::makeStreamedResetStart(ctxId, correlator))) { + return StreamResetStartResult::OutOfSpace; + } + + StreamResetStartResult result = openResetStream(serverSideStateMessages); + if(result == StreamResetStartResult::Ok) { + m_resetStreamState = ResetStreamState::Streaming; + m_resetStreamCtxId = ctxId; + m_resetStreamSize = 0; + m_resetStreamStartIndex = m_lastIndex + 1LL; + m_resetStreamMessageCount = 0; + } + + return result; +} + +bool SessionHistory::receiveResetStreamMessageCallback( + void *user, DP_Message *msg) +{ + return static_cast(user)->receiveResetStreamMessage( + net::Message::noinc(msg)); +} + +bool SessionHistory::receiveResetStreamMessage(const net::Message &msg) +{ + if(msg.isControl() || (msg.isServerMeta() && msg.type() != DP_MSG_CHAT)) { + m_resetStreamAddError = StreamResetAddResult::DisallowedType; + return false; + } + + size_t newSize = m_resetStreamSize + msg.length(); + if(m_sizeLimit > 0 && newSize > m_sizeLimit) { + m_resetStreamAddError = StreamResetAddResult::OutOfSpace; + return false; + } + m_resetStreamSize = newSize; + + StreamResetAddResult result = addResetStreamMessage(msg); + if(result == StreamResetAddResult::Ok) { + ++m_resetStreamMessageCount; + return true; + } else { + m_resetStreamAddError = result; + return false; + } +} + +StreamResetAddResult +SessionHistory::addStreamResetMessage(uint8_t ctxId, const net::Message &msg) +{ + if(m_resetStreamState != ResetStreamState::Streaming) { + return StreamResetAddResult::NotActive; + } + + if(m_resetStreamCtxId != ctxId) { + return StreamResetAddResult::InvalidUser; + } + + if(msg.type() != DP_MSG_RESET_STREAM) { + return StreamResetAddResult::BadType; + } + + size_t size; + const unsigned char *data = + DP_msg_reset_stream_data(msg.toResetStream(), &size); + if(size != 0) { + if(!m_resetStreamConsumer) { + m_resetStreamConsumer = DP_reset_stream_consumer_new( + &receiveResetStreamMessageCallback, this, false); + if(!m_resetStreamConsumer) { + abortActiveStreamedReset(); + return StreamResetAddResult::ConsumerError; + } + } + + m_resetStreamAddError = StreamResetAddResult::ConsumerError; + if(!DP_reset_stream_consumer_push(m_resetStreamConsumer, data, size)) { + Q_ASSERT(m_resetStreamAddError != StreamResetAddResult::Ok); + return m_resetStreamAddError; + } + } + return StreamResetAddResult::Ok; +} + +StreamResetAbortResult SessionHistory::abortStreamedReset(int ctxId) +{ + if(m_resetStreamState == ResetStreamState::Streaming) { + if(ctxId < 0 || ctxId == m_resetStreamCtxId) { + abortActiveStreamedReset(); + return StreamResetAbortResult::Ok; + } else { + return StreamResetAbortResult::InvalidUser; + } + } else { + return StreamResetAbortResult::NotActive; + } +} + +StreamResetPrepareResult +SessionHistory::prepareStreamedReset(uint8_t ctxId, int expectedMessageCount) +{ + if(m_resetStreamState != ResetStreamState::Streaming) { + return StreamResetPrepareResult::NotActive; + } + + if(m_resetStreamCtxId != ctxId) { + return StreamResetPrepareResult::InvalidUser; + } + + m_resetStreamAddError = StreamResetAddResult::ConsumerError; + bool freeOk = DP_reset_stream_consumer_free_finish(m_resetStreamConsumer); + m_resetStreamConsumer = nullptr; + if(!freeOk) { + switch(m_resetStreamAddError) { + case StreamResetAddResult::OutOfSpace: + return StreamResetPrepareResult::OutOfSpace; + default: + return StreamResetPrepareResult::ConsumerError; + } + } + + if(m_resetStreamMessageCount != expectedMessageCount || + expectedMessageCount == 0) { + abortActiveStreamedReset(); + return StreamResetPrepareResult::InvalidMessageCount; + } + + switch(addResetStreamMessage(net::ServerReply::makeCaughtUp(0))) { + case StreamResetAddResult::Ok: + break; + case StreamResetAddResult::OutOfSpace: + return StreamResetPrepareResult::OutOfSpace; + default: + return StreamResetPrepareResult::ConsumerError; + } + + StreamResetPrepareResult result = prepareResetStream(); + if(result == StreamResetPrepareResult::Ok) { + m_resetStreamState = ResetStreamState::Prepared; + } else { + m_resetStreamState = ResetStreamState::None; + } + + m_resetStreamCtxId = 0; + return result; +} + +bool SessionHistory::resolveStreamedReset( + long long &outOffset, QString &outError) +{ + if(m_resetStreamState != ResetStreamState::Prepared) { + outError = QStringLiteral("reset stream is not prepared"); + return false; + } + + long long newFirstIndex = m_lastIndex + 1LL; + long long messageCount; + size_t sizeInBytes; + bool ok = + resolveResetStream(newFirstIndex, messageCount, sizeInBytes, outError); + m_resetStreamState = ResetStreamState::None; + m_resetStreamCtxId = 0; + if(!ok) { + return false; + } + + m_sizeInBytes = sizeInBytes; + m_firstIndex = newFirstIndex; + m_lastIndex += messageCount; + m_autoResetBaseSize = m_resetStreamSize; + outOffset = messageCount; + return true; +} + +void SessionHistory::abortActiveStreamedReset() +{ + discardResetStream(); + m_resetStreamState = ResetStreamState::None; + m_resetStreamCtxId = 0; + DP_reset_stream_consumer_free_discard(m_resetStreamConsumer); + m_resetStreamConsumer = nullptr; +} + +size_t SessionHistory::effectiveAutoResetThreshold() const { - uint t = autoResetThreshold(); + size_t t = autoResetThreshold(); // Zero means autoreset is not enabled if(t > 0) { t += m_autoResetBaseSize; if(m_sizeLimit > 0) { - t = qMin(t, uint(m_sizeLimit * 0.9)); + t = qMin(t, size_t(m_sizeLimit * 0.9)); } } return t; } +void SessionHistory::resetAutoResetThresholdBase() +{ + m_autoResetBaseSize = m_sizeInBytes; +} + void SessionHistory::setAuthenticatedOperator(const QString &authId, bool op) { if(op) { diff --git a/src/libserver/sessionhistory.h b/src/libserver/sessionhistory.h index c953e1818e..a412572f7c 100644 --- a/src/libserver/sessionhistory.h +++ b/src/libserver/sessionhistory.h @@ -9,6 +9,8 @@ #include #include +struct DP_ResetStreamConsumer; + namespace protocol { class ProtocolVersion; } @@ -17,6 +19,47 @@ namespace server { class Client; +enum class StreamResetStartResult { + Ok, + Unsupported, + InvalidSessionState, + InvalidCorrelator, + InvalidUser, + AlreadyActive, + OutOfSpace, + WriteError, +}; + +enum class StreamResetAddResult { + Ok, + Unsupported, + InvalidUser, + NotActive, + BadType, + DisallowedType, + OutOfSpace, + WriteError, + ConsumerError, +}; + +enum class StreamResetAbortResult { + Ok, + Unsupported, + InvalidUser, + NotActive, +}; + +enum class StreamResetPrepareResult { + Ok, + Unsupported, + InvalidUser, + InvalidMessageCount, + NotActive, + OutOfSpace, + WriteError, + ConsumerError, +}; + /** * @brief Abstract base class for session history implementations * @@ -119,17 +162,20 @@ class SessionHistory : public QObject { virtual void joinUser(uint8_t id, const QString &name); //! Set the history size threshold for requesting autoreset - virtual void setAutoResetThreshold(uint limit) = 0; + virtual void setAutoResetThreshold(size_t limit) = 0; //! Get the history autoreset request threshold - virtual uint autoResetThreshold() const = 0; + virtual size_t autoResetThreshold() const = 0; //! Get the final autoreset threshold that includes the reset image base //! size - uint effectiveAutoResetThreshold() const; + size_t effectiveAutoResetThreshold() const; //! Get the reset image base size - uint autoResetThresholdBase() const { return m_autoResetBaseSize; } + size_t autoResetThresholdBase() const { return m_autoResetBaseSize; } + + //! Set the autoreset threshold base to the current size in bytes + void resetAutoResetThresholdBase(); virtual int nextCatchupKey() = 0; @@ -164,6 +210,70 @@ class SessionHistory : public QObject { */ bool reset(const net::MessageList &newHistory); + /** + * @brief Start streaming in a history reset + * + * This will store reset messages in a separate buffer, but keep the session + * running as normal. Once the entire reset snapshot has been received, that + * snapshot is taken as the new base, all other messages received meanwhile + * will be appended to it and the history switched over to that new state. + * + * This will add a stream-reset-start command message with the given context + * id and correlator to the history, which the resetting client will use as + * a marker as to where to start generating the reset image from. + * + * @return Result code, ::Ok means the stream was started, anything else is + * an error and means it wasn't. + */ + StreamResetStartResult startStreamedReset( + uint8_t ctxId, const QString &correlator, + const net::MessageList &serverSideStateMessages); + + StreamResetAddResult + addStreamResetMessage(uint8_t ctxId, const net::Message &msg); + + /** + * @brief Cancel a streaming history reset in progress + * + * @param ctxId Only cancel if the streaming context id matches this value. + * A negative value means no check is done. + * + * @return Result code, ::Ok means the stream was aborted, anything else is + * an error and means it wasn't. + */ + StreamResetAbortResult abortStreamedReset(int ctxId = -1); + + /** + * @brief Prepare a streaming history reset to replace the current history + * + * This puts the reset into a pending state, to be resolved when all clients + * are sufficiently caught up to not be inside the old state anymore. + * + * @param ctxId User performing the reset. Mismatch between this and the + * actual user will return failure, but keep the stream going. + * @param expectedMessageCount The expected amount of messages in the + * streamed reset image. Mismatch fails the reset. + * + * @return Result code, ::Ok means the reset was executed successfully, + * anything else is an error and means it wasn't. + */ + StreamResetPrepareResult + prepareStreamedReset(uint8_t ctxId, int expectedMessageCount); + + bool resolveStreamedReset(long long &outOffset, QString &outError); + + /** + * @brief Get the number of messages in the current or last streamed reset + */ + int resetStreamMessageCount() const { return m_resetStreamMessageCount; } + + bool isResetStreamPending() const + { + return m_resetStreamState == ResetStreamState::Prepared; + } + + long long resetStreamStartIndex() const { return m_resetStreamStartIndex; } + /** * @brief Get a batch of messages * @@ -174,7 +284,8 @@ class SessionHistory : public QObject { * The second element of the tuple is the index of the last message * in the batch, or lastIndex() if there were no more available messages */ - virtual std::tuple getBatch(int after) const = 0; + virtual std::tuple + getBatch(long long after) const = 0; /** * @brief Mark messages before the given index as unneeded (for now) @@ -182,7 +293,7 @@ class SessionHistory : public QObject { * This is used to inform caching history storage backends which messages * have been sent to all connected users and can thus be freed. */ - virtual void cleanupBatches(int before) = 0; + virtual void cleanupBatches(long long before) = 0; /** * @brief End this session and delete any associated files (if any) @@ -198,14 +309,14 @@ class SessionHistory : public QObject { * * @param limit maximum size in bytes or 0 for no limit */ - void setSizeLimit(uint limit) { m_sizeLimit = limit; } + void setSizeLimit(size_t limit) { m_sizeLimit = limit; } /** * @brief Get the session size limit * * If zero, the size is not limited. */ - uint sizeLimit() const { return m_sizeLimit; } + size_t sizeLimit() const { return m_sizeLimit; } /** * @brief Get the size of the history in bytes @@ -213,11 +324,14 @@ class SessionHistory : public QObject { * Note: this is the serialized size, not the size of the in-memory * representation. */ - uint sizeInBytes() const { return m_sizeInBytes; } + size_t sizeInBytes() const { return m_sizeInBytes; } - bool hasRegularSpaceFor(uint bytes) const { return hasSpaceFor(bytes, 0); } + bool hasRegularSpaceFor(size_t bytes) const + { + return hasSpaceFor(bytes, 0); + } - bool hasEmergencySpaceFor(uint bytes) const + bool hasEmergencySpaceFor(size_t bytes) const { return hasSpaceFor(bytes, 1024u * 1024u); // 1MiB of emergency space. } @@ -228,12 +342,12 @@ class SessionHistory : public QObject { * The index numbers grow monotonically during the session and are * not reset even when the history itself is reset. */ - int firstIndex() const { return m_firstIndex; } + long long firstIndex() const { return m_firstIndex; } /** * @brief Get the index number of the last message in history */ - int lastIndex() const { return m_lastIndex; } + long long lastIndex() const { return m_lastIndex; } /** * @brief Get the list of in-session IP bans @@ -353,25 +467,49 @@ class SessionHistory : public QObject { const QString &extAuthId, const QString &sid, const QString &bannedBy) = 0; virtual void historyRemoveBan(int id) = 0; - void historyLoaded(uint size, int messageCount); + void historyLoaded(size_t size, int messageCount); + + virtual StreamResetStartResult + openResetStream(const net::MessageList &serverSideStateMessages) = 0; + virtual StreamResetAddResult + addResetStreamMessage(const net::Message &msg) = 0; + virtual StreamResetPrepareResult prepareResetStream() = 0; + virtual bool resolveResetStream( + long long newFirstIndex, long long &outMessageCount, + size_t &outSizeInBytes, QString &outError) = 0; + virtual void discardResetStream() = 0; int incrementNextCatchupKey(int &nextCatchupKey); SessionBanList m_banlist; private: - bool hasSpaceFor(uint bytes, uint extra) const; - void addMessageInternal(const net::Message &msg, uint bytes); + enum class ResetStreamState { None, Streaming, Prepared }; + + bool hasSpaceFor(size_t bytes, size_t extra) const; + void addMessageInternal(const net::Message &msg, size_t bytes); + + void abortActiveStreamedReset(); + static bool receiveResetStreamMessageCallback(void *user, DP_Message *msg); + bool receiveResetStreamMessage(const net::Message &msg); QString m_id; IdQueue m_idqueue; QDateTime m_startTime; - uint m_sizeInBytes; - uint m_sizeLimit; - uint m_autoResetBaseSize; - int m_firstIndex; - int m_lastIndex; + size_t m_sizeInBytes; + size_t m_sizeLimit; + size_t m_autoResetBaseSize; + long long m_firstIndex; + long long m_lastIndex; + + ResetStreamState m_resetStreamState = ResetStreamState::None; + uint8_t m_resetStreamCtxId = 0; + size_t m_resetStreamSize = 0; + long long m_resetStreamStartIndex = 0; + int m_resetStreamMessageCount = 0; + DP_ResetStreamConsumer *m_resetStreamConsumer = nullptr; + StreamResetAddResult m_resetStreamAddError; QSet m_authOps; QSet m_authTrusted; diff --git a/src/libserver/thinserverclient.cpp b/src/libserver/thinserverclient.cpp index 61dc8842bf..3b72fad0a2 100644 --- a/src/libserver/thinserverclient.cpp +++ b/src/libserver/thinserverclient.cpp @@ -8,7 +8,7 @@ namespace server { ThinServerClient::ThinServerClient( QTcpSocket *socket, ServerLog *logger, QObject *parent) : Client(socket, logger, false, parent) - , m_historyPosition(-1) + , m_historyPosition(-1LL) { connectSendNextHistoryBatch(); } @@ -18,7 +18,7 @@ ThinServerClient::ThinServerClient( QWebSocket *socket, const QHostAddress &ip, ServerLog *logger, QObject *parent) : Client(socket, ip, logger, false, parent) - , m_historyPosition(-1) + , m_historyPosition(-1LL) { connectSendNextHistoryBatch(); } @@ -31,21 +31,25 @@ ThinServerClient::~ThinServerClient() void ThinServerClient::sendNextHistoryBatch() { + ThinSession *s = static_cast(session()); + net::MessageQueue *mq = messageQueue(); // Only enqueue messages for uploading when upload queue is empty // and session is in a normal running state. // (We'll get another messagesAvailable signal when ready) - if(session() == nullptr || messageQueue()->isUploading() || - session()->state() != Session::State::Running) - return; - - net::MessageList batch; - int batchLast; - std::tie(batch, batchLast) = - session()->history()->getBatch(m_historyPosition); - m_historyPosition = batchLast; - messageQueue()->sendMultiple(batch.size(), batch.constData()); - - static_cast(session())->cleanupHistoryCache(); + if(s && !mq->isUploading() && s->state() == Session::State::Running) { + // There may be a streamed reset pending, waiting for clients to catch + // up far enough. If the streamed reset is applies, it will change the + // history position of all clients, so don't touch it before this point! + s->resolvePendingStreamedReset(); + + net::MessageList batch; + long long batchLast; + std::tie(batch, batchLast) = s->history()->getBatch(m_historyPosition); + m_historyPosition = batchLast; + mq->sendMultiple(batch.size(), batch.constData()); + + s->cleanupHistoryCache(); + } } void ThinServerClient::connectSendNextHistoryBatch() diff --git a/src/libserver/thinserverclient.h b/src/libserver/thinserverclient.h index ea42e0beb9..e3dd3bf9cb 100644 --- a/src/libserver/thinserverclient.h +++ b/src/libserver/thinserverclient.h @@ -25,9 +25,11 @@ class ThinServerClient final : public Client { * The returned index in the index of the last history message that * is (or was) in the client's upload queue. */ - int historyPosition() const { return m_historyPosition; } + long long historyPosition() const { return m_historyPosition; } - void setHistoryPosition(int pos) { m_historyPosition = pos; } + void setHistoryPosition(long long pos) { m_historyPosition = pos; } + + void addToHistoryPosition(long long offset) { m_historyPosition += offset; } signals: void thinServerClientDestroyed(ThinServerClient *thisClient); @@ -38,7 +40,7 @@ public slots: private: void connectSendNextHistoryBatch(); - int m_historyPosition; + long long m_historyPosition; }; } diff --git a/src/libserver/thinsession.cpp b/src/libserver/thinsession.cpp index 1febdfc4db..7687d7a505 100644 --- a/src/libserver/thinsession.cpp +++ b/src/libserver/thinsession.cpp @@ -5,6 +5,8 @@ #include "libserver/thinserverclient.h" #include "libshared/net/message.h" #include "libshared/net/servercmd.h" +#include +#include namespace server { @@ -12,11 +14,18 @@ ThinSession::ThinSession( SessionHistory *history, ServerConfig *config, sessionlisting::Announcements *announcements, QObject *parent) : Session(history, config, announcements, parent) + , m_autoResetTimer(new QTimer(this)) { history->setSizeLimit(config->getConfigSize(config::SessionSizeLimit)); history->setAutoResetThreshold( config->getConfigSize(config::AutoresetThreshold)); resetLastStatusUpdate(); + m_autoResetTimer->setTimerType(Qt::VeryCoarseTimer); + m_autoResetTimer->setInterval(AUTORESET_RESPONSE_DELAY_MSECS); + m_autoResetTimer->setSingleShot(true); + connect( + m_autoResetTimer, &QTimer::timeout, this, + &ThinSession::triggerAutoReset); } void ThinSession::addToHistory(const net::Message &msg) @@ -66,53 +75,17 @@ void ThinSession::addToHistory(const net::Message &msg) } addedToHistory(msg); - - // Request auto-reset when threshold is crossed. - const uint autoResetThreshold = history()->effectiveAutoResetThreshold(); - if(autoResetThreshold > 0 && - m_autoResetRequestStatus == AutoResetState::NotSent && - history()->sizeInBytes() > autoResetThreshold) { - log(Log() - .about(Log::Level::Info, Log::Topic::Status) - .message( - QString( - "Autoreset threshold (%1, effectively %2 MB) reached.") - .arg( - history()->autoResetThreshold() / (1024.0 * 1024.0), - 0, 'g', 1) - .arg( - autoResetThreshold / (1024.0 * 1024.0), 0, 'g', - 1))); - - // Legacy alert for Drawpile 2.0.x versions - directToAll(net::ServerReply::makeSizeLimitWarning( - history()->sizeInBytes(), autoResetThreshold)); - - // New style for Drawpile 2.1.0 and newer - // Autoreset request: send an autoreset query to each logged in - // operator. The user that responds first gets to perform the reset. - net::Message reqMsg = - net::ServerReply::makeResetRequest(history()->sizeLimit(), true); - - for(Client *c : clients()) { - if(c->isOperator()) - c->sendDirectMessage(reqMsg); - } - - m_autoResetRequestStatus = AutoResetState::Queried; - } + checkAutoResetQuery(); // Regular history size status updates if(m_lastStatusUpdate.hasExpired()) { - directToAll( - net::ServerReply::makeStatusUpdate(history()->sizeInBytes())); - resetLastStatusUpdate(); + sendStatusUpdate(); } } void ThinSession::cleanupHistoryCache() { - int minIdx = history()->lastIndex(); + long long minIdx = history()->lastIndex(); for(const Client *c : clients()) { minIdx = qMin( static_cast(c)->historyPosition(), @@ -121,56 +94,383 @@ void ThinSession::cleanupHistoryCache() history()->cleanupBatches(minIdx); } -void ThinSession::readyToAutoReset(int ctxId) +void ThinSession::readyToAutoReset( + const AutoResetResponseParams ¶ms, const QString &payload) { - Client *c = getClientById(ctxId); + Client *c = nullptr; + bool haveOtherQueriedClients = false; + for(Client *candidate : clients()) { + if(candidate->id() == params.ctxId) { + c = candidate; + } else if(candidate->resetFlags().testFlag( + Client::ResetFlag::Queried)) { + haveOtherQueriedClients = true; + } + } + if(!c) { // Shouldn't happen log(Log() .about(Log::Level::Error, Log::Topic::RuleBreak) - .message(QString("Non-existent user %1 sent ready-to-autoreset") - .arg(ctxId))); + .message(QStringLiteral( + "Non-existent user %1 sent ready-to-autoreset") + .arg(params.ctxId))); return; } - if(!c->isOperator()) { + if(!c->isOperator() || c->isGhost()) { // Unlikely to happen normally, but possible if connection is // really slow and user is deopped at just the right moment + c->setResetFlags(Client::ResetFlag::None); log(Log() .about(Log::Level::Warn, Log::Topic::RuleBreak) - .message(QString("User %1 is not an operator, but sent " - "ready-to-autoreset") - .arg(ctxId))); + .message(QStringLiteral("User %1 is not an operator, but sent " + "ready-to-autoreset") + .arg(params.ctxId))); return; } - if(m_autoResetRequestStatus != AutoResetState::Queried) { - // Only the first response in handled + if(!c->resetFlags().testFlag(Client::ResetFlag::Queried)) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 responded to an autoreset " + "request without being in query state") + .arg(params.ctxId))); + return; + } + + + if(m_autoResetRequestStatus != AutoResetState::Queried && + m_autoResetRequestStatus != AutoResetState::QueriedWaiting) { + c->setResetFlags(Client::ResetFlag::None); log(Log() .about(Log::Level::Debug, Log::Topic::Status) .message( - QString( + QStringLiteral( "User %1 was late to respond to an autoreset request") - .arg(ctxId))); + .arg(params.ctxId))); + return; + } + + if(!payload.isEmpty() && payload != m_autoResetPayload) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 responded with incorrect " + "autoreset payload") + .arg(params.ctxId))); return; } + int responseRank = m_autoResetCandidates.isEmpty() + ? 1 + : m_autoResetCandidates.last().responseRank + 1; + m_autoResetCandidates.append(AutoResetCandidate{ + responseRank, params.ctxId, params.capabilities, params.osQuality, + params.netQuality, params.averagePing}); + + c->setResetFlags(Client::ResetFlag::Responded); + + if(m_autoResetRequestStatus == AutoResetState::Queried) { + m_autoResetRequestStatus = AutoResetState::QueriedWaiting; + if(haveOtherQueriedClients) { + m_autoResetTimer->start(AUTORESET_RESPONSE_DELAY_MSECS); + } else { + m_autoResetTimer->stop(); + triggerAutoReset(); + } + } else if(!haveOtherQueriedClients) { + m_autoResetTimer->stop(); + triggerAutoReset(); + } +} + +StreamResetStartResult +ThinSession::handleStreamResetStart(int ctxId, const QString &correlator) +{ + if(m_autoResetRequestStatus != AutoResetState::Requested) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 tried to start stream reset, " + "but session is in state %2") + .arg(ctxId) + .arg(int(m_autoResetRequestStatus)))); + return StreamResetStartResult::InvalidSessionState; + } + + Client *c = getClientById(ctxId); + if(!c) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 tried to start stream reset, " + "but doesn't exist") + .arg(ctxId))); + return StreamResetStartResult::InvalidUser; + } + + Client::ResetFlags resetFlags = c->resetFlags(); + if(!resetFlags.testFlag(Client::ResetFlag::Awaiting) || + !resetFlags.testFlag(Client::ResetFlag::Streaming)) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral( + "User %1 tried to start stream reset, but we're " + "not waiting for a streamed reset from them") + .arg(ctxId))); + return StreamResetStartResult::InvalidUser; + } + + if(!c->isOperator() || c->isGhost()) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 tried to start stream reset, " + "but isn't an operator") + .arg(ctxId))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + return StreamResetStartResult::InvalidUser; + } + + if(correlator != m_autoResetPayload) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message( + QStringLiteral("User %1 tried to start stream reset, with " + "a correlator not matching payload") + .arg(ctxId))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + return StreamResetStartResult::InvalidCorrelator; + } + + StreamResetStartResult result = history()->startStreamedReset( + ctxId, correlator, serverSideStateMessages()); + + bool clear = false; + QString error; + switch(result) { + case StreamResetStartResult::Ok: + c->setResetFlags( + result == StreamResetStartResult::Ok ? Client::ResetFlag::Streaming + : Client::ResetFlag::None); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message( + QStringLiteral("User %1 started stream reset").arg(ctxId))); + return StreamResetStartResult::Ok; + case StreamResetStartResult::Unsupported: + error = QStringLiteral("that's unsupported by this session"); + break; + case StreamResetStartResult::InvalidSessionState: + error = QStringLiteral("we're not anticipating one"); + break; + case StreamResetStartResult::InvalidCorrelator: + error = QStringLiteral("the correlator doesn't match"); + break; + case StreamResetStartResult::InvalidUser: + error = QStringLiteral("they're not the one tasked to do it"); + break; + case StreamResetStartResult::AlreadyActive: + error = QStringLiteral("it's already started"); + break; + case StreamResetStartResult::OutOfSpace: + error = QStringLiteral("the session is out of space"); + break; + case StreamResetStartResult::WriteError: + error = QStringLiteral("a write error occurred"); + break; + } + if(error.isEmpty()) { + error = QStringLiteral("unknown error %1 occurred").arg(int(result)); + } + log(Log() - .about(Log::Level::Info, Log::Topic::Status) - .message(QString("User %1 responded to autoreset request first") - .arg(ctxId))); + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral("User %1 tried to start stream reset, but %2") + .arg(ctxId) + .arg(error))); - c->sendDirectMessage( - net::ServerReply::makeResetRequest(history()->sizeLimit(), false)); + if(clear) { + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + } - m_autoResetRequestStatus = AutoResetState::Requested; + return result; +} + +StreamResetAbortResult ThinSession::handleStreamResetAbort(int ctxId) +{ + Client *c = getClientById(ctxId); + if(!c) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 tried to abort stream reset, " + "but doesn't exist") + .arg(ctxId))); + return StreamResetAbortResult::InvalidUser; + } + + StreamResetAbortResult result = history()->abortStreamedReset(ctxId); + + QString error; + switch(result) { + case StreamResetAbortResult::Ok: + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message( + QStringLiteral("User %1 aborted stream reset").arg(ctxId))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + return StreamResetAbortResult::Ok; + case StreamResetAbortResult::Unsupported: + error = QStringLiteral("that's unsupported by this session"); + break; + case StreamResetAbortResult::InvalidUser: + error = QStringLiteral("isn't the one performing it"); + break; + case StreamResetAbortResult::NotActive: + error = QStringLiteral("none is active"); + break; + } + if(error.isEmpty()) { + error = QStringLiteral("unknown error %1 occurred").arg(int(result)); + } + + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral("User %1 tried to abort stream reset, but %2") + .arg(ctxId) + .arg(error))); + + return result; +} + +StreamResetPrepareResult +ThinSession::handleStreamResetFinish(int ctxId, int expectedMessageCount) +{ + Client *c = getClientById(ctxId); + if(!c) { + log(Log() + .about(Log::Level::Warn, Log::Topic::RuleBreak) + .message(QStringLiteral("User %1 tried to finish stream reset " + "with %2 messages but doesn't exist") + .arg(ctxId) + .arg(expectedMessageCount))); + return StreamResetPrepareResult::InvalidUser; + } + + StreamResetPrepareResult result = + history()->prepareStreamedReset(ctxId, expectedMessageCount); + + bool clear = false; + QString error; + switch(result) { + case StreamResetPrepareResult::Ok: + c->setResetFlags(Client::ResetFlag::None); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message(QStringLiteral("User %1 prepared streamed reset") + .arg(ctxId))); + resolvePendingStreamedReset(); + return StreamResetPrepareResult::Ok; + case StreamResetPrepareResult::Unsupported: + error = QStringLiteral("that's unsupported by this session"); + break; + case StreamResetPrepareResult::InvalidUser: + error = QStringLiteral("isn't the one performing it"); + break; + case StreamResetPrepareResult::InvalidMessageCount: + clear = true; + error = QStringLiteral("the actual message count is %1") + .arg(history()->resetStreamMessageCount()); + break; + case StreamResetPrepareResult::NotActive: + error = QStringLiteral("none is active"); + break; + case StreamResetPrepareResult::OutOfSpace: + clear = true; + error = QStringLiteral("the reset image is too large"); + break; + case StreamResetPrepareResult::WriteError: + clear = true; + error = QStringLiteral("a write error occurred"); + break; + case StreamResetPrepareResult::ConsumerError: + clear = true; + error = QStringLiteral("a stream consumer error occurred: %1") + .arg(DP_error()); + break; + } + + if(error.isEmpty()) { + error = QStringLiteral("unknown error %1 occurred").arg(int(result)); + } + + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message(QStringLiteral("User %1 tried to finish stream reset with " + "%2 messages, but %3") + .arg(ctxId) + .arg(expectedMessageCount) + .arg(error))); + + if(clear) { + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + } + + return result; +} + +void ThinSession::resolvePendingStreamedReset() +{ + SessionHistory *hist = history(); + if(hist->isResetStreamPending() && allClientsCaughtUpToResetStreamStart()) { + size_t prevSizeInBytes = hist->sizeInBytes(); + size_t prevAutoResetThresholdBase = hist->autoResetThresholdBase(); + long long offset; + QString error; + if(hist->resolveStreamedReset(offset, error)) { + Q_ASSERT(offset > 0); + QLocale locale = QLocale::c(); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message( + QStringLiteral( + "Resolved streamed reset with offset %1 (size %2 " + "=> %3, autoreset threshold base %4 => %5)") + .arg(offset) + .arg(locale.formattedDataSize(prevSizeInBytes)) + .arg(locale.formattedDataSize(hist->sizeInBytes())) + .arg(locale.formattedDataSize( + prevAutoResetThresholdBase)) + .arg(locale.formattedDataSize( + hist->autoResetThresholdBase())))); + for(Client *c : clients()) { + ThinServerClient *tsc = static_cast(c); + tsc->addToHistoryPosition(offset); + } + clearAutoReset(); + sendStatusUpdate(); + } else { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral("Error resolving streamed reset: %1") + .arg(error))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + } + } +} + +void ThinSession::onSessionInitialized() +{ + clearAutoReset(); + sendStatusUpdate(); } void ThinSession::onSessionReset() { + clearAutoReset(); directToAll(net::ServerReply::makeCatchup( history()->lastIndex() - history()->firstIndex(), 0)); - m_autoResetRequestStatus = AutoResetState::NotSent; + sendStatusUpdate(); history()->addMessage(net::ServerReply::makeCaughtUp(0)); } @@ -191,7 +491,345 @@ void ThinSession::onClientJoin(Client *client, bool host) client->sendDirectMessage(net::ServerReply::makeCatchup( history()->lastIndex() - history()->firstIndex(), caughtUpAdded ? catchupKey : -1)); + client->sendDirectMessage( + net::ServerReply::makeStatusUpdate(int(history()->sizeInBytes()))); + } +} + +void ThinSession::onClientLeave(Client *client) +{ + Session::onClientLeave(client); + uint8_t ctxId = client->id(); + invalidateAutoResetCandidate(ctxId); + if(history()->abortStreamedReset(ctxId) == StreamResetAbortResult::Ok) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral("User %1 left while streaming autoreset") + .arg(ctxId))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + } else { + resolvePendingStreamedReset(); + } +} + +void ThinSession::onClientDeop(Client *client) +{ + uint8_t ctxId = client->id(); + invalidateAutoResetCandidate(ctxId); + if(history()->abortStreamedReset(ctxId) == StreamResetAbortResult::Ok) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral("User %1 deopped while streaming autoreset") + .arg(ctxId))); + client->sendDirectMessage( + net::ServerReply::makeStreamedResetProgress(0, true)); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + } +} + +void ThinSession::onResetStream(Client &client, const net::Message &msg) +{ + StreamResetAddResult result = + history()->addStreamResetMessage(client.id(), msg); + + bool clear = false; + QString error; + switch(result) { + case StreamResetAddResult::Ok: + client.sendDirectMessage( + net::ServerReply::makeStreamedResetProgress(0, false)); + return; + case StreamResetAddResult::Unsupported: + error = QStringLiteral("that's unsupported by this session"); + break; + case StreamResetAddResult::InvalidUser: + error = QStringLiteral("they're not the one tasked to do it"); + break; + case StreamResetAddResult::BadType: + clear = true; + error = QStringLiteral("the message has bad type %1 (%2)") + .arg(msg.type()) + .arg(msg.typeName()); + break; + case StreamResetAddResult::DisallowedType: + clear = true; + error = QStringLiteral("of an illegal message in the reset image"); + break; + case StreamResetAddResult::NotActive: + error = QStringLiteral("no stream is active"); + break; + case StreamResetAddResult::OutOfSpace: + clear = true; + error = QStringLiteral("the session is out of space"); + break; + case StreamResetAddResult::WriteError: + clear = true; + error = QStringLiteral("a write error occurred"); + break; + case StreamResetAddResult::ConsumerError: + clear = true; + error = QStringLiteral("a stream consumer error occurred: %1") + .arg(DP_error()); + break; + } + + if(error.isEmpty()) { + error = QStringLiteral("unknown error %1 occurred").arg(int(result)); + } + + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message(QStringLiteral( + "User %1 tried to add a stream reset message, but %2") + .arg(client.id()) + .arg(error))); + client.sendDirectMessage( + net::ServerReply::makeStreamedResetProgress(0, true)); + + if(clear) { + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + } +} + +void ThinSession::onStateChanged() +{ + // No matter what state we changed into, any autoreset dealings are moot. + clearAutoReset(); +} + +void ThinSession::sendStatusUpdate() +{ + directToAll( + net::ServerReply::makeStatusUpdate(int(history()->sizeInBytes()))); + resetLastStatusUpdate(); +} + +void ThinSession::checkAutoResetQuery() +{ + // Query for an autoreset only if the session is in a running state, we + // haven't queried yet and we're not delaying after a failed autoreset. + if(state() != State::Running || + m_autoResetRequestStatus != AutoResetState::NotSent || + !m_autoResetDelay.hasExpired()) { + return; + } + + // Only query for an autoreset if the threshold has been reached. + size_t autoResetThreshold = history()->effectiveAutoResetThreshold(); + if(history()->sizeInBytes() <= autoResetThreshold) { + return; + } + + QLocale locale = QLocale::c(); + log(Log() + .about(Log::Level::Info, Log::Topic::Status) + .message( + QString("Autoreset threshold (%1, effectively %2) reached.") + .arg(locale.formattedDataSize( + history()->autoResetThreshold())) + .arg(locale.formattedDataSize(autoResetThreshold)))); + + // Legacy alert for Drawpile 2.0.x versions + directToAll(net::ServerReply::makeSizeLimitWarning( + int(history()->sizeInBytes()), int(autoResetThreshold))); + + // New style for Drawpile 2.1.0 and newer + // Autoreset request: send an autoreset query to each logged in + // operator. The user that responds first gets to perform the reset. + // Since Drawpile 2.2.2: also send a payload along to indicate extended + // autoreset capabilities for candidate picking and streamed autoresets. + m_autoResetPayload = generateAutoResetPayload(); + net::Message reqMsg = net::ServerReply::makeResetQuery( + int(history()->sizeLimit()), m_autoResetPayload); + + for(Client *c : clients()) { + if(c->isOperator() && !c->isGhost()) { + c->setResetFlags(Client::ResetFlag::Queried); + c->sendDirectMessage(reqMsg); + } else { + c->setResetFlags(Client::ResetFlag::None); + } + } + + m_autoResetCandidates.clear(); + m_autoResetRequestStatus = AutoResetState::Queried; + m_autoResetTimer->start(AUTORESET_RESPONSE_DELAY_MSECS); +} + +QString ThinSession::generateAutoResetPayload() +{ + static uint32_t autoResetIndex; + return QStringLiteral("1:%1:%2") + .arg(autoResetIndex++) + .arg(QDateTime::currentMSecsSinceEpoch()); +} + +bool ThinSession::AutoResetCandidate::operator<( + const AutoResetCandidate &other) const +{ + // Context id <= 0 means this is not a valid candidate, e.g. because they + // lost op or they left outright. + if(ctxId <= 0) { + if(other.ctxId > 0) { + return true; + } + } else if(other.ctxId <= 0) { + return false; + } + + // Clients that can do a streamed reset are preferable. + if(!capabilities.testFlag(ResetCapability::GzipStream)) { + if(other.capabilities.testFlag(ResetCapability::GzipStream)) { + return true; + } + } else if(!other.capabilities.testFlag(ResetCapability::GzipStream)) { + return false; + } + + // Clients on a better operating system are preferable. + if(osQuality < other.osQuality) { + return true; + } + + // Clients on a better network are preferable. At the time of writing, this + // is only determined by the user manually toggling a setting for it. + if(netQuality < other.netQuality) { + return true; + } + + // Clients with a lower average ping are preferable. Average pings <= 0.0 + // means that the client didn't give us any data, so we can't compare. + if(averagePing > 0.0 && other.averagePing > 0.0 && + averagePing > other.averagePing) { + return true; + } + + // Finally, whoever responded first gets the cut. + return other.responseRank < responseRank; +} + +void ThinSession::triggerAutoReset() +{ + m_autoResetTimer->stop(); + + // Queried means we asked for someone to reset the session, but nobody + // answered. This can legitimately happen if there's no operators or if all + // operators have their clients configured to not perform autoresets. + if(m_autoResetRequestStatus == AutoResetState::Queried) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message(QString( + "Autoreset failed with no candidates to perform it"))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + return; + } + + if(m_autoResetRequestStatus != AutoResetState::QueriedWaiting) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message(QString("Autoreset triggered unexpectedly in state %1") + .arg(int(m_autoResetRequestStatus)))); + return; + } + + Client *c = nullptr; + bool stream = false; + std::sort(m_autoResetCandidates.begin(), m_autoResetCandidates.end()); + for(auto it = m_autoResetCandidates.rbegin(), + end = m_autoResetCandidates.rend(); + it != end; ++it) { + int ctxId = it->ctxId; + if(ctxId > 0) { + Client *candidate = getClientById(ctxId); + if(!candidate) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral( + "User %1 is gone, can't autoreset with them") + .arg(ctxId))); + } else if(!candidate->isOperator() || candidate->isGhost()) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message( + QStringLiteral("User %1 is not an operator, can't " + "autoreset with them") + .arg(ctxId))); + } else { + c = candidate; + stream = it->capabilities.testFlag(ResetCapability::GzipStream); + break; + } + } + } + + if(!c) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message(QStringLiteral("Autoreset triggered, but no user " + "available to execute it with"))); + clearAutoReset(AUTORESET_FAILURE_RETRY_MSECS); + return; + } + + m_autoResetRequestStatus = AutoResetState::Requested; + m_autoResetCandidates.clear(); + + if(stream) { + c->setResetFlags( + Client::ResetFlag::Awaiting | Client::ResetFlag::Streaming); + c->sendDirectMessage(net::ServerReply::makeStreamedResetRequest( + int(history()->sizeLimit()), m_autoResetPayload, + QStringLiteral("gzip1"))); + } else { + c->setResetFlags(Client::ResetFlag::Awaiting); + c->sendDirectMessage( + net::ServerReply::makeResetRequest(int(history()->sizeLimit()))); + } +} + +void ThinSession::invalidateAutoResetCandidate(int ctxId) +{ + for(AutoResetCandidate &arc : m_autoResetCandidates) { + if(arc.ctxId == ctxId) { + arc.ctxId = -1; + } + } +} + +void ThinSession::clearAutoReset(int retryDelay) +{ + if(m_autoResetTimer) { + m_autoResetTimer->stop(); + } + + m_autoResetRequestStatus = AutoResetState::NotSent; + for(Client *c : clients()) { + c->setResetFlags(Client::ResetFlag::None); + } + + if(history()->abortStreamedReset() == StreamResetAbortResult::Ok) { + log(Log() + .about(Log::Level::Warn, Log::Topic::Status) + .message(QStringLiteral( + "Streamed reset aborted by auto reset clear"))); + } + + m_autoResetDelay.setRemainingTime(retryDelay > 0 ? retryDelay : 0); +} + +bool ThinSession::allClientsCaughtUpToResetStreamStart() const +{ + long long resetStreamStartIndex = history()->resetStreamStartIndex(); + for(Client *c : clients()) { + ThinServerClient *tsc = static_cast(c); + if(tsc->historyPosition() < resetStreamStartIndex) { + return false; + } } + return true; } } diff --git a/src/libserver/thinsession.h b/src/libserver/thinsession.h index ffc1cf0fed..aa536d9fd8 100644 --- a/src/libserver/thinsession.h +++ b/src/libserver/thinsession.h @@ -4,6 +4,8 @@ #include "libserver/session.h" #include +class QTimer; + namespace server { /** @@ -17,7 +19,18 @@ class ThinSession final : public Session { sessionlisting::Announcements *announcements, QObject *parent = nullptr); - void readyToAutoReset(int ctxId) override; + void readyToAutoReset( + const AutoResetResponseParams ¶ms, const QString &payload) override; + + StreamResetStartResult + handleStreamResetStart(int ctxId, const QString &correlator) override; + + StreamResetAbortResult handleStreamResetAbort(int ctxId) override; + + StreamResetPrepareResult + handleStreamResetFinish(int ctxId, int expectedMessageCount) override; + + void resolvePendingStreamedReset(); void cleanupHistoryCache(); @@ -25,21 +38,53 @@ class ThinSession final : public Session { protected: void addToHistory(const net::Message &msg) override; + void onSessionInitialized() override; void onSessionReset() override; void onClientJoin(Client *client, bool host) override; + void onClientLeave(Client *client) override; + void onClientDeop(Client *client) override; + void onResetStream(Client &client, const net::Message &msg) override; + void onStateChanged() override; private: - enum class AutoResetState { NotSent, Queried, Requested }; + // Give up on autoreset requests after 3 minutes. + static constexpr int AUTORESET_GIVE_UP_DELAY_MSECS = 180000; + // Wait for up to 3 seconds after a client responded to an autorest query. + static constexpr int AUTORESET_RESPONSE_DELAY_MSECS = 3000; + // After an autoreset failed, wait 30 seconds before trying again. + static constexpr int AUTORESET_FAILURE_RETRY_MSECS = 30000; + + enum class AutoResetState { NotSent, Queried, QueriedWaiting, Requested }; - void resetLastStatusUpdate() - { - m_lastStatusUpdate.setRemainingTime(10000); - } + struct AutoResetCandidate { + int responseRank; + int ctxId; + ResetCapabilities capabilities; + int osQuality; + qreal netQuality; + qreal averagePing; + + bool operator<(const AutoResetCandidate &other) const; + }; + + void sendStatusUpdate(); + void resetLastStatusUpdate() { m_lastStatusUpdate.setRemainingTime(10000); } + + void checkAutoResetQuery(); + static QString generateAutoResetPayload(); + void triggerAutoReset(); + void invalidateAutoResetCandidate(int ctxId); + void clearAutoReset(int delay = 0); + + bool allClientsCaughtUpToResetStreamStart() const; QDeadlineTimer m_lastStatusUpdate; QDeadlineTimer m_lastSizeWarning; - + QDeadlineTimer m_autoResetDelay; AutoResetState m_autoResetRequestStatus = AutoResetState::NotSent; + QTimer *m_autoResetTimer; + QString m_autoResetPayload; + QVector m_autoResetCandidates; }; } diff --git a/src/libshared/net/message.cpp b/src/libshared/net/message.cpp index b102adf3cf..1ac17fcfa6 100644 --- a/src/libshared/net/message.cpp +++ b/src/libshared/net/message.cpp @@ -127,6 +127,12 @@ bool Message::isInCommandRange() const return type() >= 128; } +bool Message::isAllowedInResetImage() const +{ + DP_MessageType t = type(); + return int(t) >= 64 || t == DP_MSG_CHAT; +} + Message Message::asEmergencyMessage() const { switch(type()) { @@ -258,6 +264,12 @@ DP_MsgPutImage *Message::toPutImage() const return static_cast(DP_message_internal(m_data)); } +DP_MsgResetStream *Message::toResetStream() const +{ + Q_ASSERT(type() == DP_MSG_RESET_STREAM); + return static_cast(DP_message_internal(m_data)); +} + DP_MsgServerCommand *Message::toServerCommand() const { Q_ASSERT(type() == DP_MSG_SERVER_COMMAND); @@ -416,6 +428,11 @@ makeSessionOwnerMessage(uint8_t contextId, const QVector &users) const_cast(users.constData()))); } +Message makeSoftResetMessage(uint8_t contextId) +{ + return Message::noinc(DP_msg_soft_reset_new(contextId)); +} + Message makeTrustedUsersMessage(uint8_t contextId, const QVector &users) { diff --git a/src/libshared/net/message.h b/src/libshared/net/message.h index 62a6d3c5cb..c401a9a505 100644 --- a/src/libshared/net/message.h +++ b/src/libshared/net/message.h @@ -48,6 +48,7 @@ class Message final { bool isControl() const; bool isServerMeta() const; bool isInCommandRange() const; + bool isAllowedInResetImage() const; // Returns user joins without the avatar; leaves and session owners as-is. // Any other message type results in a null message, since they're not @@ -76,6 +77,7 @@ class Message final { DP_MsgLayerTreeCreate *toLayerTreeCreate() const; DP_MsgPrivateChat *toPrivateChat() const; DP_MsgPutImage *toPutImage() const; + DP_MsgResetStream *toResetStream() const; DP_MsgServerCommand *toServerCommand() const; DP_MsgSessionOwner *toSessionOwner() const; DP_MsgTrustedUsers *toTrustedUsers() const; @@ -122,6 +124,8 @@ Message makeServerCommandMessage(uint8_t contextId, const QJsonDocument &msg); Message makeSessionOwnerMessage(uint8_t contextId, const QVector &users); +Message makeSoftResetMessage(uint8_t contextId); + Message makeTrustedUsersMessage(uint8_t contextId, const QVector &users); diff --git a/src/libshared/net/servercmd.cpp b/src/libshared/net/servercmd.cpp index 8dfff1fe00..00e7310f7d 100644 --- a/src/libshared/net/servercmd.cpp +++ b/src/libshared/net/servercmd.cpp @@ -40,6 +40,41 @@ net::Message ServerCommand::makeUnannounce(const QString &url) return make("unlist-session", QJsonArray() << url); } +QString ServerCommand::autoresetOs() +{ +#if defined(__EMSCRIPTEN__) + return QStringLiteral("emscripten"); +#elif defined(Q_OS_ANDROID) + return QStringLiteral("android"); +#elif defined(Q_OS_IOS) + return QStringLiteral("ios"); +#elif defined(Q_OS_WIN) + return QStringLiteral("windows"); +#elif defined(Q_OS_MACOS) + return QStringLiteral("macos"); +#elif defined(Q_OS_LINUX) + return QStringLiteral("linux"); +#elif defined(Q_OS_UNIX) + return QStringLiteral("unix"); +#else + return QStringLiteral("unknown"); +#endif +} + +int ServerCommand::rateAutoresetOs(const QString &os) +{ + if(os == QStringLiteral("emscripten") || os == QStringLiteral("android") || + os == QStringLiteral("ios")) { + return -1; // Bad candidates. Memory limits and they fall asleep easily. + } else if( + os == QStringLiteral("windows") || os == QStringLiteral("macos") || + os == QStringLiteral("linux") || os == QStringLiteral("unix")) { + return 1; // Good candidates. Desktop operating systems. + } else { + return 0; // Unknown OS or client didn't specify. + } +} + net::Message ServerCommand::toMessage() const { QJsonObject data{{QStringLiteral("cmd"), cmd}}; @@ -145,6 +180,10 @@ ServerReply ServerReply::fromJson(const QJsonDocument &doc) r.type = ServerReply::ReplyType::BanImpEx; } else if(typestr == QStringLiteral("outofspace")) { r.type = ServerReply::ReplyType::OutOfSpace; + } else if(typestr == QStringLiteral("sstart")) { + r.type = ServerReply::ReplyType::StreamStart; + } else if(typestr == QStringLiteral("sprogress")) { + r.type = ServerReply::ReplyType::StreamProgress; } else { r.type = ServerReply::ReplyType::Unknown; } @@ -345,12 +384,50 @@ ServerReply::makeReset(const QString &message, const QString &state) {QStringLiteral("state"), state}}); } -net::Message ServerReply::makeResetRequest(int maxSize, bool query) +net::Message ServerReply::makeResetQuery(int maxSize, const QString &payload) { return make( {{QStringLiteral("type"), QStringLiteral("autoreset")}, {QStringLiteral("maxSize"), maxSize}, - {QStringLiteral("query"), query}}); + {QStringLiteral("query"), true}, + {QStringLiteral("payload"), payload}}); +} + +net::Message ServerReply::makeResetRequest(int maxSize) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("autoreset")}, + {QStringLiteral("maxSize"), maxSize}, + {QStringLiteral("query"), false}}); +} + +net::Message ServerReply::makeStreamedResetRequest( + int maxSize, const QString &correlator, const QString &stream) +{ + return make( + {{QStringLiteral("type"), QStringLiteral("autoreset")}, + {QStringLiteral("maxSize"), maxSize}, + {QStringLiteral("query"), false}, + {QStringLiteral("correlator"), correlator}, + {QStringLiteral("stream"), stream}}); +} + +net::Message ServerReply::makeStreamedResetStart( + uint8_t contextId, const QString &correlator) +{ + QJsonObject data = { + {QStringLiteral("type"), QStringLiteral("sstart")}, + {QStringLiteral("correlator"), correlator}}; + return net::makeServerCommandMessage(contextId, QJsonDocument{data}); +} + +net::Message +ServerReply::makeStreamedResetProgress(uint8_t contextId, bool cancel) +{ + QJsonObject data = { + {QStringLiteral("type"), QStringLiteral("sprogress")}, + {QStringLiteral("cancel"), cancel}}; + return net::makeServerCommandMessage(contextId, QJsonDocument{data}); } net::Message ServerReply::makeResultHostLookup(const QString &message) diff --git a/src/libshared/net/servercmd.h b/src/libshared/net/servercmd.h index 659ccca9d6..345c5cf6cc 100644 --- a/src/libshared/net/servercmd.h +++ b/src/libshared/net/servercmd.h @@ -41,6 +41,12 @@ struct ServerCommand { //! Request the server to remove an announcement at the listing server static net::Message makeUnannounce(const QString &url); + + //! Returns the operating for use in an autoreset response + static QString autoresetOs(); + + //! Rates the quality of the given autoreset OS, higher is better + static int rateAutoresetOs(const QString &os); }; /** @@ -92,6 +98,8 @@ struct ServerReply { CaughtUp, // previous catchup is complete BanImpEx, // session ban import/export OutOfSpace, // session is out of space, block local drawing + StreamStart, // streamed session reset start marker + StreamProgress, // streamed session reset progress control message } type; QString message; QJsonObject reply; @@ -159,7 +167,14 @@ struct ServerReply { static net::Message makeReset(const QString &message, const QString &state); - static net::Message makeResetRequest(int maxSize, bool query); + static net::Message makeResetQuery(int maxSize, const QString &payload); + static net::Message makeResetRequest(int maxSize); + static net::Message makeStreamedResetRequest( + int maxSize, const QString &correlator, const QString &stream); + static net::Message + makeStreamedResetStart(uint8_t contextId, const QString &correlator); + static net::Message + makeStreamedResetProgress(uint8_t contextId, bool cancel); static net::Message makeResultHostLookup(const QString &message);