summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Meeks <michael.meeks@collabora.com>2019-07-04 10:50:33 +0100
committerMichael Meeks <michael.meeks@collabora.com>2019-08-05 22:21:54 -0400
commit9e791fb0d4d4c701c50b8b50091b5665b832c3f1 (patch)
tree3588dfda4e66e48caa1e74dbad1da743c8fea7d5
parentwsd: add 'meta origin' to clipboardcontent payloads too (diff)
downloadonline-9e791fb0d4d4c701c50b8b50091b5665b832c3f1.tar.gz
online-9e791fb0d4d4c701c50b8b50091b5665b832c3f1.zip
clipboard: persist selections for a while after a view closes.
re-factor ClientSession state to be a simpler state machine. Have a nice disconnect / disconnected handshake on view close. Change-Id: Ie933cc5c7dfab46c66f4d38a4d75c459aa1cff87
-rw-r--r--common/Clipboard.hpp64
-rw-r--r--kit/ChildSession.cpp2
-rw-r--r--kit/ChildSession.hpp3
-rw-r--r--kit/Kit.cpp6
-rw-r--r--test/UnitCopyPaste.cpp18
-rw-r--r--wsd/ClientSession.cpp121
-rw-r--r--wsd/ClientSession.hpp33
-rw-r--r--wsd/DocumentBroker.cpp158
-rw-r--r--wsd/DocumentBroker.hpp9
-rw-r--r--wsd/LOOLWSD.cpp14
-rw-r--r--wsd/LOOLWSD.hpp2
11 files changed, 373 insertions, 57 deletions
diff --git a/common/Clipboard.hpp b/common/Clipboard.hpp
index fbbcb557c8..0b4bee4389 100644
--- a/common/Clipboard.hpp
+++ b/common/Clipboard.hpp
@@ -13,9 +13,13 @@
#include <string>
#include <vector>
+#include <unordered_map>
+#include <mutex>
+
#include <stdlib.h>
#include <Log.hpp>
#include <Exceptions.hpp>
+#include <Poco/MemoryStream.h>
struct ClipboardData
{
@@ -78,6 +82,66 @@ struct ClipboardData
}
};
+/// Used to store expired view's clipboards
+class ClipboardCache
+{
+ std::mutex _mutex;
+ struct Entry {
+ std::chrono::steady_clock::time_point _inserted;
+ std::shared_ptr<std::string> _rawData; // big.
+ };
+ // clipboard key -> data
+ std::unordered_map<std::string, Entry> _cache;
+public:
+ ClipboardCache()
+ {
+ }
+
+ void insertClipboard(const std::string key[2],
+ const char *data, size_t size)
+ {
+ if (size == 0)
+ {
+ LOG_TRC("clipboard cache - ignores empty clipboard data");
+ return;
+ }
+ Entry ent;
+ ent._inserted = std::chrono::steady_clock::now();
+ ent._rawData = std::make_shared<std::string>(data, size);
+ LOG_TRC("insert cached clipboard: " + key[0] + " and " + key[1]);
+ std::lock_guard<std::mutex> lock(_mutex);
+ _cache[key[0]] = ent;
+ _cache[key[1]] = ent;
+ }
+
+ std::shared_ptr<std::string> getClipboard(const std::string &key)
+ {
+ std::lock_guard<std::mutex> lock(_mutex);
+ std::shared_ptr<std::string> data;
+ auto it = _cache.find(key);
+ if (it != _cache.end())
+ data = it->second._rawData;
+ return data;
+ }
+
+ void checkexpiry()
+ {
+ std::lock_guard<std::mutex> lock(_mutex);
+ auto now = std::chrono::steady_clock::now();
+ LOG_TRC("check expiry of cached clipboards");
+ for (auto it = _cache.begin(); it != _cache.end();)
+ {
+ if (std::chrono::duration_cast<std::chrono::minutes>(now - it->second._inserted).count() >= 10)
+ {
+ LOG_TRC("expiring expiry of cached clipboard: " + it->first);
+ it = _cache.erase(it);
+ }
+ else
+ ++it;
+ }
+ }
+};
+
#endif
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index 56700852ec..b3a3fd8b8c 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -946,7 +946,7 @@ bool ChildSession::getTextSelection(const char* /*buffer*/, int /*length*/, cons
return true;
}
-bool ChildSession::getClipboard(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens )
+bool ChildSession::getClipboard(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens)
{
const char **pMimeTypes = nullptr; // fetch all for now.
const char *pOneType[2];
diff --git a/kit/ChildSession.hpp b/kit/ChildSession.hpp
index b15c8b33aa..a1a4532bb4 100644
--- a/kit/ChildSession.hpp
+++ b/kit/ChildSession.hpp
@@ -233,6 +233,8 @@ public:
using Session::sendTextFrame;
+ bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
+
private:
bool loadDocument(const char* buffer, int length, const std::vector<std::string>& tokens);
@@ -245,7 +247,6 @@ private:
bool downloadAs(const char* buffer, int length, const std::vector<std::string>& tokens);
bool getChildId();
bool getTextSelection(const char* buffer, int length, const std::vector<std::string>& tokens);
- bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
bool setClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
std::string getTextSelectionInternal(const std::string& mimeType);
bool paste(const char* buffer, int length, const std::vector<std::string>& tokens);
diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index 832eb64235..a76b6d1f02 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -983,7 +983,7 @@ public:
// session is being removed.
for (auto it = _sessions.cbegin(); it != _sessions.cend(); )
{
- if (it->second->isCloseFrame())
+ if (it->second->isCloseFrame())
{
deadSessions.push_back(it->second);
it = _sessions.erase(it);
@@ -1901,6 +1901,10 @@ private:
_editorId = -1;
}
LOG_DBG("Removing ChildSession [" << sessionId << "].");
+
+ // Tell them we're going quietly.
+ session->sendTextFrame("disconnected:");
+
_sessions.erase(it);
const size_t count = _sessions.size();
LOG_DBG("Have " << count << " child" << (count == 1 ? "" : "ren") <<
diff --git a/test/UnitCopyPaste.cpp b/test/UnitCopyPaste.cpp
index 23697778c5..018c60ea76 100644
--- a/test/UnitCopyPaste.cpp
+++ b/test/UnitCopyPaste.cpp
@@ -109,7 +109,7 @@ public:
std::string value;
// allow empty clipboards
- if (clipboard && mimeType =="" && clipboard->size() == 0)
+ if (clipboard && mimeType == "" && content == "")
return true;
if (!clipboard || !clipboard->findType(mimeType, value))
@@ -205,7 +205,9 @@ public:
assert(sessions.size() > 0 && session < sessions.size());
clientSession = sessions[session];
- return clientSession->getClipboardURI(false); // nominally thread unsafe
+ std::string tag = clientSession->getClipboardURI(false); // nominally thread unsafe
+ std::cerr << "Got tag '" << tag << "' for session " << session << "\n";
+ return tag;
}
std::string buildClipboardText(const std::string &text)
@@ -294,6 +296,18 @@ public:
if (!fetchClipboardAssert(clipURI, "text/plain;charset=utf-8", "herring"))
return;
+ std::cerr << "Close sockets:\n";
+ socket->shutdown();
+ socket2->shutdown();
+
+ sleep(1); // paranoia.
+
+ std::cerr << "Fetch clipboards after shutdown:\n";
+ if (!fetchClipboardAssert(clipURI2, "text/plain;charset=utf-8", "kippers"))
+ return;
+ if (!fetchClipboardAssert(clipURI, "text/plain;charset=utf-8", "herring"))
+ return;
+
std::cerr << "Clipboard tests succeeded" << std::endl;
exitTest(TestResult::Ok);
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index e02d9dadf9..29a72a9d5b 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -27,6 +27,7 @@
#include <common/Common.hpp>
#include <common/Log.hpp>
#include <common/Protocol.hpp>
+#include <common/Clipboard.hpp>
#include <common/Session.hpp>
#include <common/Unit.hpp>
#include <common/Util.hpp>
@@ -48,8 +49,7 @@ ClientSession::ClientSession(const std::string& id,
_docBroker(docBroker),
_uriPublic(uriPublic),
_isDocumentOwner(false),
- _isAttached(false),
- _isViewLoaded(false),
+ _state(SessionState::DETACHED),
_keyEvents(1),
_clientVisibleArea(0, 0, 0, 0),
_clientSelectedPart(-1),
@@ -67,6 +67,9 @@ ClientSession::ClientSession(const std::string& id,
// populate with random values.
for (auto it : _clipboardKeys)
rotateClipboardKey(false);
+
+ // get timestamp set
+ setState(SessionState::DETACHED);
}
// Can't take a reference in the constructor.
@@ -85,11 +88,81 @@ ClientSession::~ClientSession()
SessionMap.erase(getId());
}
+static const char *stateToString(ClientSession::SessionState s)
+{
+ switch (s)
+ {
+ case ClientSession::SessionState::DETACHED: return "detached";
+ case ClientSession::SessionState::LOADING: return "loading";
+ case ClientSession::SessionState::LIVE: return "live";
+ case ClientSession::SessionState::WAIT_DISCONNECT: return "wait_disconnect";
+ }
+ return "invalid";
+}
+
+void ClientSession::setState(SessionState newState)
+{
+ LOG_TRC("ClientSession: transition from " << stateToString(_state) <<
+ " to " << stateToString(newState));
+ switch (newState)
+ {
+ case SessionState::DETACHED:
+ assert(_state == SessionState::DETACHED);
+ break;
+ case SessionState::LOADING:
+ assert(_state == SessionState::DETACHED);
+ break;
+ case SessionState::LIVE:
+ assert(_state == SessionState::LIVE ||
+ _state == SessionState::LOADING);
+ break;
+ case SessionState::WAIT_DISCONNECT:
+ assert(_state == SessionState::LOADING ||
+ _state == SessionState::LIVE);
+ break;
+ }
+ _state = newState;
+ _lastStateTime = std::chrono::steady_clock::now();
+}
+
+bool ClientSession::disconnectFromKit()
+{
+ assert(_state != SessionState::WAIT_DISCONNECT);
+ auto docBroker = getDocumentBroker();
+ if (_state == SessionState::LIVE && docBroker)
+ {
+ setState(SessionState::WAIT_DISCONNECT);
+
+ LOG_TRC("request/rescue clipboard on disconnect for " << getId());
+ // rescue clipboard before shutdown.
+ docBroker->forwardToChild(getId(), "getclipboard");
+
+ // handshake nicely; so wait for 'disconnected'
+ docBroker->forwardToChild(getId(), "disconnect");
+
+ return false;
+ }
+
+ return true; // just get on with it
+}
+
+// Allow 20secs for the clipboard and disconection to come.
+bool ClientSession::staleWaitDisconnect(const std::chrono::steady_clock::time_point &now)
+{
+ if (_state != SessionState::WAIT_DISCONNECT)
+ return false;
+ return std::chrono::duration_cast<std::chrono::seconds>(now - _lastStateTime).count() >= 20;
+}
+
void ClientSession::rotateClipboardKey(bool notifyClient)
{
if (_wopiFileInfo && _wopiFileInfo->getDisableCopy())
return;
+ if (_state != SessionState::LIVE && // editing
+ _state != SessionState::DETACHED) // constructor
+ return;
+
_clipboardKeys[1] = _clipboardKeys[0];
_clipboardKeys[0] = Util::rng::getHardRandomHexString(16);
LOG_TRC("Clipboard key on [" << getId() << "] set to " << _clipboardKeys[0] <<
@@ -135,14 +208,39 @@ bool ClientSession::matchesClipboardKeys(const std::string &/*viewId*/, const st
return false;
}
+
void ClientSession::handleClipboardRequest(DocumentBroker::ClipboardRequest type,
const std::shared_ptr<StreamSocket> &socket,
+ const std::string &tag,
const std::shared_ptr<std::string> &data)
{
// Move the socket into our DocBroker.
auto docBroker = getDocumentBroker();
docBroker->addSocketToPoll(socket);
+ if (_state == SessionState::WAIT_DISCONNECT)
+ {
+ LOG_TRC("Clipboard request " << tag << " for disconnecting session");
+ if (docBroker->lookupSendClipboardTag(socket, tag, false))
+ return; // the getclipboard already completed.
+ if (type == DocumentBroker::CLIP_REQUEST_SET)
+ {
+ std::ostringstream oss;
+ oss << "HTTP/1.1 400 Bad Request\r\n"
+ << "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
+ << "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
+ << "Content-Length: 0\r\n"
+ << "\r\n";
+ socket->send(oss.str());
+ socket->shutdown();
+ }
+ else // will be handled during shutdown
+ {
+ LOG_TRC("Clipboard request " << tag << " queued for shutdown");
+ _clipSockets.push_back(socket);
+ }
+ }
+
std::string specific;
if (type == DocumentBroker::CLIP_REQUEST_GET_RICH_HTML_ONLY)
specific = " text/html";
@@ -1107,7 +1205,8 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
// 'download' and/or providing our helpful / user page.
// for now just for remote sockets.
- LOG_TRC("Got clipboard content to send to " << _clipSockets.size() << "sockets");
+ LOG_TRC("Got clipboard content of size " << payload->size() << " to send to " <<
+ _clipSockets.size() << " sockets in state " << stateToString(_state));
postProcessCopyPayload(payload);
@@ -1116,6 +1215,13 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
if (payload->data()[header++] == '\n')
break;
const bool empty = header >= payload->size();
+
+ // final cleanup ...
+ if (!empty && _state == SessionState::WAIT_DISCONNECT &&
+ (!_wopiFileInfo || !_wopiFileInfo->getDisableCopy()))
+ LOOLWSD::SavedClipboards->insertClipboard(
+ _clipboardKeys, &payload->data()[header], payload->size() - header);
+
for (auto it : _clipSockets)
{
std::ostringstream oss;
@@ -1139,6 +1245,11 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
}
_clipSockets.clear();
return true;
+ } else if (tokens[0] == "disconnected:") {
+
+ LOG_INF("End of disconnection handshake for " << getId());
+ docBroker->finalRemoveSession(getId());
+ return true;
}
if (!isDocPasswordProtected())
@@ -1149,7 +1260,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
}
else if (tokens[0] == "status:")
{
- setViewLoaded();
+ setState(ClientSession::SessionState::LIVE);
docBroker->setLoaded();
// Wopi post load actions
if (_wopiFileInfo && !_wopiFileInfo->getTemplateSource().empty())
@@ -1454,7 +1565,7 @@ void ClientSession::dumpState(std::ostream& os)
os << "\t\tisReadOnly: " << isReadOnly()
<< "\n\t\tisDocumentOwner: " << _isDocumentOwner
- << "\n\t\tisAttached: " << _isAttached
+ << "\n\t\tstate: " << stateToString(_state)
<< "\n\t\tkeyEvents: " << _keyEvents
// << "\n\t\tvisibleArea: " << _clientVisibleArea
<< "\n\t\tclientSelectedPart: " << _clientSelectedPart
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 5d5d68249e..d4b31dbeac 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -44,13 +44,21 @@ public:
void setReadOnly() override;
- /// Returns true if this session is added to a DocBroker.
- bool isAttached() const { return _isAttached; }
- void setAttached() { _isAttached = true; }
+ enum SessionState {
+ DETACHED, // initial
+ LOADING, // attached to a DocBroker & waiting for load
+ LIVE, // Document is loaded & editable or viewable.
+ WAIT_DISCONNECT // closed and waiting for Kit's disconnected message
+ };
/// Returns true if this session has loaded a view (i.e. we got status message).
- bool isViewLoaded() const { return _isViewLoaded; }
- void setViewLoaded() { _isViewLoaded = true; }
+ bool isViewLoaded() const { return _state == SessionState::LIVE; }
+
+ /// returns true if we're waiting for the kit to acknowledge disconnect.
+ bool inWaitDisconnected() const { return _state == SessionState::WAIT_DISCONNECT; }
+
+ /// transition to a new state
+ void setState(SessionState newState);
void setDocumentOwner(const bool documentOwner) { _isDocumentOwner = documentOwner; }
bool isDocumentOwner() const { return _isDocumentOwner; }
@@ -61,6 +69,9 @@ public:
/// Integer id of the view in the kit process, or -1 if unknown
int getKitViewId() const { return _kitViewId; }
+ /// Disconnect the session and do final cleanup, @returns true if we should not wait.
+ bool disconnectFromKit();
+
// sendTextFrame that takes std::string and string literal.
using Session::sendTextFrame;
@@ -147,6 +158,7 @@ public:
/// Handle a clipboard fetch / put request.
void handleClipboardRequest(DocumentBroker::ClipboardRequest type,
const std::shared_ptr<StreamSocket> &socket,
+ const std::string &tag,
const std::shared_ptr<std::string> &data);
/// Create URI for transient clipboard content.
@@ -155,6 +167,9 @@ public:
/// Adds and/or modified the copied payload before sending on to the client.
void postProcessCopyPayload(std::shared_ptr<Message> payload);
+ /// Returns true if we're expired waiting for a clipboard and should be removed
+ bool staleWaitDisconnect(const std::chrono::steady_clock::time_point &now);
+
/// Generate and rotate a new clipboard hash, sending it if appropriate
void rotateClipboardKey(bool notifyClient);
@@ -211,11 +226,11 @@ private:
/// The socket to which the converted (saveas) doc is sent.
std::shared_ptr<StreamSocket> _saveAsSocket;
- /// If we are added to a DocBroker.
- bool _isAttached;
+ /// The phase of our lifecycle that we're in.
+ SessionState _state;
- /// If we have loaded a view.
- bool _isViewLoaded;
+ /// Time of last state transition
+ std::chrono::steady_clock::time_point _lastStateTime;
/// Wopi FileInfo object
std::unique_ptr<WopiStorage::WOPIFileInfo> _wopiFileInfo;
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 2d46457479..be1794eb7a 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -35,6 +35,7 @@
#include "TileCache.hpp"
#include <common/Log.hpp>
#include <common/Message.hpp>
+#include <common/Clipboard.hpp>
#include <common/Protocol.hpp>
#include <common/Unit.hpp>
#include <common/FileUtil.hpp>
@@ -343,10 +344,23 @@ void DocumentBroker::pollThread()
#if !MOBILEAPP
if (std::chrono::duration_cast<std::chrono::minutes>(now - lastClipboardHashUpdateTime).count() >= 2)
+ for (auto &it : _sessions)
+ {
+ if (it.second->staleWaitDisconnect(now))
+ {
+ std::string id = it.second->getId();
+ LOG_WRN("Unusual, Kit session " + id + " failed its disconnect handshake, killing");
+ finalRemoveSession(id);
+ break; // it invalid.
+ }
+ }
+
+ if (std::chrono::duration_cast<std::chrono::minutes>(now - lastClipboardHashUpdateTime).count() >= 5)
{
LOG_TRC("Rotating clipboard keys");
- for (auto& it : _sessions)
+ for (auto &it : _sessions)
it.second->rotateClipboardKey(true);
+
lastClipboardHashUpdateTime = now;
}
@@ -810,7 +824,7 @@ bool DocumentBroker::saveToStorage(const std::string& sessionId,
// If marked to destroy, or session is disconnected, remove.
const auto it = _sessions.find(sessionId);
if (_markToDestroy || (it != _sessions.end() && it->second->isCloseFrame()))
- removeSessionInternal(sessionId);
+ disconnectSessionInternal(sessionId);
// If marked to destroy, then this was the last session.
if (_markToDestroy || _sessions.empty())
@@ -1025,7 +1039,8 @@ bool DocumentBroker::autoSave(const bool force, const bool dontSaveIfUnmodified)
for (auto& sessionIt : _sessions)
{
// Save the document using an editable session, or first ...
- if (savingSessionId.empty() || !sessionIt.second->isReadOnly())
+ if (savingSessionId.empty() ||
+ (!sessionIt.second->isReadOnly() && !sessionIt.second->inWaitDisconnected()))
{
savingSessionId = sessionIt.second->getId();
}
@@ -1211,7 +1226,7 @@ size_t DocumentBroker::addSessionInternal(const std::shared_ptr<ClientSession>&
// Add and attach the session.
_sessions.emplace(session->getId(), session);
- session->setAttached();
+ session->setState(ClientSession::SessionState::LOADING);
const size_t count = _sessions.size();
LOG_TRC("Added " << (session->isReadOnly() ? "readonly" : "non-readonly") <<
@@ -1247,7 +1262,7 @@ size_t DocumentBroker::removeSession(const std::string& id)
// If last editable, save and don't remove until after uploading to storage.
if (!lastEditableSession || !autoSave(isPossiblyModified(), dontSaveIfUnmodified))
- removeSessionInternal(id);
+ disconnectSessionInternal(id);
}
catch (const std::exception& ex)
{
@@ -1257,7 +1272,7 @@ size_t DocumentBroker::removeSession(const std::string& id)
return _sessions.size();
}
-size_t DocumentBroker::removeSessionInternal(const std::string& id)
+void DocumentBroker::disconnectSessionInternal(const std::string& id)
{
assertCorrectThread();
try
@@ -1272,10 +1287,51 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id)
LOOLWSD::dumpEndSessionTrace(getJailId(), id, _uriOrig);
#endif
+ LOG_TRC("Disconnect session internal " << id);
+
+ bool hardDisconnect;
+ if (it->second->inWaitDisconnected())
+ {
+ LOG_TRC("hard disconnecting while waiting for disconnected handshake.");
+ hardDisconnect = true;
+ }
+ else
+ {
+ hardDisconnect = it->second->disconnectFromKit();
+
+ // Let the child know the client has disconnected.
+ const std::string msg("child-" + id + " disconnect");
+ _childProcess->sendTextFrame(msg);
+ }
+
+ if (hardDisconnect)
+ finalRemoveSession(id);
+ // else wait for disconnected.
+ }
+ else
+ {
+ LOG_TRC("Session [" << id << "] not found to disconnect from docKey [" <<
+ _docKey << "]. Have " << _sessions.size() << " sessions.");
+ }
+ }
+ catch (const std::exception& ex)
+ {
+ LOG_ERR("Error while disconnecting session [" << id << "]: " << ex.what());
+ }
+}
+
+void DocumentBroker::finalRemoveSession(const std::string& id)
+{
+ assertCorrectThread();
+ try
+ {
+ auto it = _sessions.find(id);
+ if (it != _sessions.end())
+ {
const bool readonly = (it->second ? it->second->isReadOnly() : false);
// Remove. The caller must have a reference to the session
- // in question, lest we destroy from underneith them.
+ // in question, lest we destroy from underneath them.
_sessions.erase(it);
const size_t count = _sessions.size();
@@ -1291,11 +1347,7 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id)
LOG_END(logger, true);
}
- // Let the child know the client has disconnected.
- const std::string msg("child-" + id + " disconnect");
- _childProcess->sendTextFrame(msg);
-
- return count;
+ return;
}
else
{
@@ -1307,8 +1359,6 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id)
{
LOG_ERR("Error while removing session [" << id << "]: " << ex.what());
}
-
- return _sessions.size();
}
void DocumentBroker::addCallback(const SocketPoll::CallbackFn& fn)
@@ -1333,7 +1383,8 @@ void DocumentBroker::alertAllUsers(const std::string& msg)
LOG_DBG("Alerting all users of [" << _docKey << "]: " << msg);
for (auto& it : _sessions)
{
- it.second->enqueueSendMessage(payload);
+ if (!it.second->inWaitDisconnected())
+ it.second->enqueueSendMessage(payload);
}
}
@@ -1421,7 +1472,8 @@ void DocumentBroker::handleTileRequest(TileDesc& tile,
{
for (auto& it: _sessions)
{
- tileCache().subscribeToTileRendering(tile, it.second);
+ if (!it.second->inWaitDisconnected())
+ tileCache().subscribeToTileRendering(tile, it.second);
}
}
else
@@ -1515,19 +1567,34 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
sendRequestedTiles(session);
}
-void DocumentBroker::handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket,
- const std::string &viewId, const std::string &tag,
- const std::shared_ptr<std::string> &data)
+/// lookup in global clipboard cache and send response, send error if missing if @sendError
+bool DocumentBroker::lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket,
+ const std::string &tag, bool sendError)
{
- for (auto& it : _sessions)
+ LOG_TRC("Clipboard request " << tag << " not for a live session - check cache.");
+ std::shared_ptr<std::string> saved =
+ LOOLWSD::SavedClipboards->getClipboard(tag);
+ if (saved)
{
- if (it.second->matchesClipboardKeys(viewId, tag))
- {
- it.second->handleClipboardRequest(type, socket, data);
- return;
- }
+ std::ostringstream oss;
+ oss << "HTTP/1.1 200 OK\r\n"
+ << "Last-Modified: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
+ << "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
+ << "Content-Length: " << saved->length() << "\r\n"
+ << "Content-Type: application/octet-stream\r\n"
+ << "X-Content-Type-Options: nosniff\r\n"
+ << "\r\n";
+ oss.write(saved->c_str(), saved->length());
+ socket->setSocketBufferSize(std::min(saved->length() + 256,
+ size_t(Socket::MaximumSendBufferSize)));
+ socket->send(oss.str());
+ socket->shutdown();
+ LOG_INF("Found and queued clipboard response for send of size " << saved->length());
+ return true;
}
- LOG_ERR("Could not find matching session to handle clipboard request for " << viewId << " tag: " << tag);
+
+ if (!sendError)
+ return false;
// Bad request.
std::ostringstream oss;
@@ -1539,6 +1606,24 @@ void DocumentBroker::handleClipboardRequest(ClipboardRequest type, const std::s
<< "Failed to find this clipboard";
socket->send(oss.str());
socket->shutdown();
+
+ return false;
+}
+
+void DocumentBroker::handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket,
+ const std::string &viewId, const std::string &tag,
+ const std::shared_ptr<std::string> &data)
+{
+ for (auto& it : _sessions)
+ {
+ if (it.second->matchesClipboardKeys(viewId, tag))
+ {
+ it.second->handleClipboardRequest(type, socket, tag, data);
+ return;
+ }
+ }
+ if (!lookupSendClipboardTag(socket, tag, true))
+ LOG_ERR("Could not find matching session to handle clipboard request for " << viewId << " tag: " << tag);
}
void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& session)
@@ -1724,7 +1809,8 @@ bool DocumentBroker::haveAnotherEditableSession(const std::string& id) const
{
if (it.second->getId() != id &&
it.second->isViewLoaded() &&
- !it.second->isReadOnly())
+ !it.second->isReadOnly() &&
+ !it.second->inWaitDisconnected())
{
// This is a loaded session that is non-readonly.
return true;
@@ -1820,9 +1906,10 @@ bool DocumentBroker::forwardToClient(const std::shared_ptr<Message>& payload)
// Broadcast to all.
// Events could cause the removal of sessions.
std::map<std::string, std::shared_ptr<ClientSession>> sessions(_sessions);
- for (const auto& pair : sessions)
+ for (const auto& it : _sessions)
{
- pair.second->handleKitToClientMessage(data, size);
+ if (!it.second->inWaitDisconnected())
+ it.second->handleKitToClientMessage(data, size);
}
}
else
@@ -1862,11 +1949,16 @@ void DocumentBroker::shutdownClients(const std::string& closeReason)
std::shared_ptr<ClientSession> session = pair.second;
try
{
- // Notify the client and disconnect.
- session->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, closeReason);
+ if (session->inWaitDisconnected())
+ finalRemoveSession(session->getId());
+ else
+ {
+ // Notify the client and disconnect.
+ session->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, closeReason);
- // Remove session, save, and mark to destroy.
- removeSession(session->getId());
+ // Remove session, save, and mark to destroy.
+ removeSession(session->getId());
+ }
}
catch (const std::exception& exc)
{
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 719f748568..b50f6d706d 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -231,6 +231,9 @@ public:
/// Flag for termination. Note that this doesn't save any unsaved changes in the document
void stop(const std::string& reason);
+ /// Hard removes a session by ID, only for ClientSession.
+ void finalRemoveSession(const std::string& id);
+
/// Thread safe termination of this broker if it has a lingering thread
void joinThread();
@@ -322,6 +325,8 @@ public:
void handleClipboardRequest(ClipboardRequest type, const std::shared_ptr<StreamSocket> &socket,
const std::string &viewId, const std::string &tag,
const std::shared_ptr<std::string> &data);
+ static bool lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket,
+ const std::string &tag, bool sendError = false);
bool isMarkedToDestroy() const { return _markToDestroy || _stop; }
@@ -401,8 +406,8 @@ private:
/// Loads a new session and adds to the sessions container.
size_t addSessionInternal(const std::shared_ptr<ClientSession>& session);
- /// Removes a session by ID. Returns the new number of sessions.
- size_t removeSessionInternal(const std::string& id);
+ /// Starts the Kit <-> DocumentBroker shutdown handshake
+ void disconnectSessionInternal(const std::string& id);
/// Forward a message from child session to its respective client session.
bool forwardToClient(const std::shared_ptr<Message>& payload);
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index a7f5b89715..bbc3fa8fb3 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -104,6 +104,7 @@ using Poco::Net::PartHandler;
#include "Auth.hpp"
#include "ClientSession.hpp"
#include <Common.hpp>
+#include <Clipboard.hpp>
#include <Crypto.hpp>
#include <DelaySocket.hpp>
#include "DocumentBroker.hpp"
@@ -721,6 +722,7 @@ static std::string UnitTestLibrary;
unsigned int LOOLWSD::NumPreSpawnedChildren = 0;
std::unique_ptr<TraceFileWriter> LOOLWSD::TraceDumper;
+std::unique_ptr<ClipboardCache> LOOLWSD::SavedClipboards;
/// This thread polls basic web serving, and handling of
/// websockets before upgrade: when upgraded they go to the
@@ -1154,6 +1156,8 @@ void LOOLWSD::initialize(Application& self)
}
#if !MOBILEAPP
+ SavedClipboards.reset(new ClipboardCache());
+
FileServerRequestHandler::initialize();
#endif
@@ -2152,7 +2156,7 @@ private:
StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM);
if (reqPathTokens.count() > 1 && reqPathTokens[0] == "lool" && reqPathTokens[1] == "clipboard")
{
-// Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
+ Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
handleClipboardRequest(request, message, disposition);
}
else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
@@ -2325,7 +2329,8 @@ private:
Poco::MemoryInputStream& message,
SocketDisposition &disposition)
{
- LOG_DBG("Clipboard request: " << request.getURI());
+ LOG_DBG("Clipboard " << ((request.getMethod() == HTTPRequest::HTTP_GET) ? "GET" : "POST") <<
+ " request: " << request.getURI());
Poco::URI requestUri(request.getURI());
Poco::URI::QueryParameters params = requestUri.getQueryParameters();
@@ -2391,7 +2396,10 @@ private:
});
});
LOG_TRC("queued clipboard command " << type << " on docBroker fetch");
- } else {
+ }
+ // fallback to persistent clipboards if we can
+ else if (!DocumentBroker::lookupSendClipboardTag(_socket.lock(), tag, false))
+ {
LOG_ERR("Invalid clipboard request: " << serverId << " with tag " << tag <<
" and broker: " << (docBroker ? "not" : "") << "found");
diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp
index a23a4f714a..0af2caa284 100644
--- a/wsd/LOOLWSD.hpp
+++ b/wsd/LOOLWSD.hpp
@@ -28,6 +28,7 @@
class ChildProcess;
class TraceFileWriter;
class DocumentBroker;
+class ClipboardCache;
std::shared_ptr<ChildProcess> getNewChild_Blocks(
#if MOBILEAPP
@@ -69,6 +70,7 @@ public:
static bool AnonymizeUsernames;
static std::atomic<unsigned> NumConnections;
static std::unique_ptr<TraceFileWriter> TraceDumper;
+ static std::unique_ptr<ClipboardCache> SavedClipboards;
static std::set<std::string> EditFileExtensions;
static unsigned MaxConnections;
static unsigned MaxDocuments;