diff options
author | Roland Reichwein <mail@reichwein.it> | 2023-01-26 20:46:30 +0100 |
---|---|---|
committer | Roland Reichwein <mail@reichwein.it> | 2023-01-26 20:46:30 +0100 |
commit | 789e5555ab4c44a1ae779eccf6ccf8340602cf22 (patch) | |
tree | fd1c15ac38ec4d43965d8e12a149ae52a0808a73 | |
parent | 004db5e7e4e9ab6ac5b4730873c6b8f58da92930 (diff) |
Websockets: Notify other clients of changes
-rwxr-xr-x | Makefile | 4 | ||||
-rw-r--r-- | connectionregistry.cpp | 95 | ||||
-rw-r--r-- | connectionregistry.h | 46 | ||||
-rw-r--r-- | debian/whiteboard.conf | 26 | ||||
-rw-r--r-- | html/whiteboard.js | 409 | ||||
-rw-r--r-- | tests/Makefile | 5 | ||||
-rw-r--r-- | whiteboard.conf | 2 | ||||
-rw-r--r-- | whiteboard.cpp | 100 | ||||
-rw-r--r-- | whiteboard.h | 8 |
9 files changed, 373 insertions, 322 deletions
@@ -13,7 +13,7 @@ VERSION=$(shell dpkg-parsechangelog --show-field Version) TGZNAME=$(PROJECTNAME)-$(VERSION).tar.xz INCLUDES=-I. -HEADERS=config.h qrcode.h storage.h whiteboard.h compiledsql.h +HEADERS=config.h qrcode.h storage.h whiteboard.h compiledsql.h connectionregistry.h SOURCES=$(HEADERS:.h=.cpp) OBJECTS=$(HEADERS:.h=.o) TARGETS=whiteboard @@ -21,7 +21,7 @@ TARGETS=whiteboard build: $(TARGETS) all: build - ./start.sh + ./whiteboard -c whiteboard.conf install: mkdir -p $(DESTDIR)/usr/bin diff --git a/connectionregistry.cpp b/connectionregistry.cpp new file mode 100644 index 0000000..1e48a96 --- /dev/null +++ b/connectionregistry.cpp @@ -0,0 +1,95 @@ +#include "connectionregistry.h" + +#include <iostream> +#include <utility> + +void ConnectionRegistry::setId(ConnectionRegistry::connection c, const std::string& id) +{ + if (!m_connections.at(c).empty()) { + auto& connections_set{m_ids.at(id)}; + connections_set.erase(c); + if (connections_set.empty()) + m_ids.erase(id); + } + + m_connections.at(c) = id; + + if (!id.empty()) { + if (!m_ids.contains(id)) + m_ids[id] = {}; + m_ids.at(id).insert(c); + } + +} + +void ConnectionRegistry::addConnection(ConnectionRegistry::connection c) +{ + if (m_connections.contains(c)) + throw std::runtime_error("ConnectionRegistry::addConnection: connection already exists"); + + m_connections.emplace(c, ""); + + std::cout << "Info: Added connection, now " << m_connections.size() << std::endl; +} + +void ConnectionRegistry::delConnection(ConnectionRegistry::connection c) +{ + if (!m_connections.contains(c)) + throw std::runtime_error("ConnectionRegistry::delConnection: connection doesn't exist"); + + std::string id {m_connections.at(c)}; + + m_connections.erase(c); + + if (!id.empty()) { + auto& connections_set{m_ids.at(id)}; + connections_set.erase(c); + if (connections_set.empty()) + m_ids.erase(id); + } + + std::cout << "Info: Deleted connection, now " << m_connections.size() << std::endl; +} + +std::unordered_set<ConnectionRegistry::connection>::iterator ConnectionRegistry::begin(const std::string& id) +{ + return m_ids.at(id).begin(); +} + +std::unordered_set<ConnectionRegistry::connection>::iterator ConnectionRegistry::end(const std::string& id) +{ + return m_ids.at(id).end(); +} + +void ConnectionRegistry::dump() +{ + std::cout << "Connection Registry:" << std::endl; + + std::cout << "Connections: " << std::endl; + for (auto& i: m_connections) { + std::cout << " " << i.first << ": " << i.second << std::endl; + } + + std::cout << "IDs: " << std::endl; + for (auto& i: m_ids) { + std::cout << " " << i.first << ":"; + + for (auto& j: i.second) { + std::cout << " " << j; + } + std::cout << std::endl; + } +} + +ConnectionRegistry::RegistryGuard::RegistryGuard(ConnectionRegistry& registry, ConnectionRegistry::connection c): + m_registry{registry}, + m_connection{c} +{ + m_registry.addConnection(m_connection); +} + +ConnectionRegistry::RegistryGuard::~RegistryGuard() +{ + m_registry.delConnection(m_connection); +} + diff --git a/connectionregistry.h b/connectionregistry.h new file mode 100644 index 0000000..2b14553 --- /dev/null +++ b/connectionregistry.h @@ -0,0 +1,46 @@ +#pragma once + +#include <algorithm> +#include <memory> +#include <unordered_map> +#include <unordered_set> + +#include <boost/asio/ip/tcp.hpp> +#include <boost/beast/websocket.hpp> + +class ConnectionRegistry +{ +public: + using connection = std::shared_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>; + + ConnectionRegistry() = default; + ~ConnectionRegistry() = default; + + void setId(connection c, const std::string& id); + + // used via RegistryGuard + void addConnection(connection c); + void delConnection(connection c); + + // map connection to id + std::unordered_map<connection, std::string> m_connections; + // map id to list of related connections, used for iteration over connections to notify about changes + std::unordered_map<std::string, std::unordered_set<connection>> m_ids; + + std::unordered_set<connection>::iterator begin(const std::string& id); + std::unordered_set<connection>::iterator end(const std::string& id); + + void dump(); + + class RegistryGuard + { + public: + RegistryGuard(ConnectionRegistry& registry, connection c); + ~RegistryGuard(); + private: + ConnectionRegistry& m_registry; + connection m_connection; + }; + +}; + diff --git a/debian/whiteboard.conf b/debian/whiteboard.conf new file mode 100644 index 0000000..126bef5 --- /dev/null +++ b/debian/whiteboard.conf @@ -0,0 +1,26 @@ +<config> + <!-- + datapath: location in filesystem to store whiteboard data + Example: /var/lib/whiteboard + --> + <datapath>/var/lib/whiteboard</datapath> + + <!-- + port: socket to listen on for websocket + Example: ::1:8765 + --> + <port>::1:8765</port> + + <!-- + Maximum age of individual whiteboard pages in seconds. + Older pages will be removed by whiteboard-cleanup and crond. + Example: 2592000 (~30 days) + --> + <maxage>2592000</maxage> + + <!-- + Number of threads to use + Example: 4 + --> + <threads>4</threads> +</config> diff --git a/html/whiteboard.js b/html/whiteboard.js index 7777d4e..9610468 100644 --- a/html/whiteboard.js +++ b/html/whiteboard.js @@ -3,145 +3,126 @@ function init() { init_board(); } -class AdjustingTimer { - constructor() { - this.update_counter = 0; // counting seconds since last counter reset - } +var revision; - fast_mode() { - if (this.update_counter < 5*60) - return true; - return false; - } +// helper for breaking feedback loop +var caretpos = 0; - // private method - // returns current interval in ms - current_update_interval() - { - if (this.fast_mode()) - return 2000; // 2s - else - return 5 * 60000; // 5min - }; +function showQRWindow() +{ + document.getElementById("qrwindow").style.display = 'block'; +} - // private - count() { - this.update_counter += this.current_update_interval() / 1000; - }; +function hideQRWindow() +{ + document.getElementById("qrwindow").style.display = 'none'; +} - // private - on_timeout() { - this.m_fn(); - this.count(); - var _this = this; - this.update_timer = setTimeout(function(){_this.on_timeout();}, this.current_update_interval()); - }; +var websocket; - // to be called once on startup - start(fn) { - this.m_fn = fn; - var _this = this; - this.update_timer = setTimeout(function(){_this.on_timeout();}, this.current_update_interval()); - }; +// +// Callbacks for websocket data of different types +// - // to be called on activity: - // * changes from remote - // * changes by ourselves - // * local activity, e.g. mouse move, or key presses - reset() { - if (!this.fast_mode()) { - this.update_counter = 0; - clearTimeout(this.update_timer); - var _this = this; - this.update_timer = setTimeout(function(){_this.on_timeout();}, this.current_update_interval()); - } else { - this.update_counter = 0; - } - }; +function on_getfile(data, rev, pos) +{ + var board = document.getElementById("board"); + if (board.value != data) { + board.value = data; + } + textAreaSetPos("board", pos); + revision = rev; } -var timer = new AdjustingTimer(); +function on_newid(id) +{ + var new_location = document.location.origin + document.location.pathname + '?id=' + id; + window.location.href = new_location; +} -function showQRWindow() +function on_qrcode(png) { - document.getElementById("qrwindow").style.display = 'block'; + var blob = new Blob([png], {type: 'image/png'}); + var url = URL.createObjectURL(blob); + var img = document.getElementById("qrcode"); + img.src = url; + showQRWindow(); } -function hideQRWindow() +function on_modify_ack(rev) { - document.getElementById("qrwindow").style.display = 'none'; + revision = rev; } -function init_board() { - var xhr = new XMLHttpRequest(); +function on_message(e) { + var parser = new DOMParser(); + var xmlDocument = parser.parseFromString(e.data, "text/xml"); - const searchParams = (new URL(document.location)).searchParams; - if (!searchParams.has('id')) { - redirect_to_new_page(); - return; - } - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - var file = new Blob([this.response]); - reader = new FileReader(); - reader.onload = function() { - var board = document.getElementById("board"); - var pos = reader.result.indexOf('\x01'); - if (pos == -1) { // not found - board.value = reader.result; - } else { - board.value = reader.result.substr(0, pos) + reader.result.substr(pos + 1); - } - textAreaSetPos("board", pos); - - // Initialization done. Now we can start modifying. - board.addEventListener("input", function() {on_modify(); }); - board.addEventListener("selectionchange", function() {on_modify(); }); - - // Initialization done. Now we can start modifying. - document.addEventListener("mousemove", function() {timer.reset(); }); - - timer.start(checkupdate); - } - - reader.readAsBinaryString(file); - - //set_status(""); // OK + var type = xmlDocument.getElementsByTagName("type")[0].textContent; + + if (type == "getfile") { + on_getfile(xmlDocument.getElementsByTagName("data")[0].textContent, + parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent), + parseInt(xmlDocument.getElementsByTagName("cursorpos")[0].textContent)); + } else if (type == "modify") { + on_modify_ack(parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent)); + } else if (type == "newid") { + on_newid(xmlDocument.getElementsByTagName("id")[0].textContent); + } else if (type == "qrcode") { + on_qrcode(xmlDocument.getElementsByTagName("png")[0].textContent); + } else if (type == "error") { + alert(xmlDocument.getElementsByTagName("message")[0].textContent); + } else { + alert("Unhandled message type: " + e.data + "|" + type); } +} - var parser = new DOMParser(); - var xmlDocument = parser.parseFromString("<request></request>", "text/xml"); - - var requestElement = xmlDocument.getElementsByTagName("request")[0]; +function handleSelection() { + const activeElement = document.activeElement - var commandElement = xmlDocument.createElement("command"); - commandElement.appendChild(document.createTextNode("getfile")); - requestElement.appendChild(commandElement); + if (activeElement && activeElement.id === 'board') { + if (caretpos != activeElement.selectionStart) { + on_selectionchange(activeElement.selectionStart); + caretpos = activeElement.selectionStart; + } + } +} - var idElement = xmlDocument.createElement("id"); - idElement.appendChild(document.createTextNode(get_id())); - requestElement.appendChild(idElement); +function init_board() { + var newlocation = location.origin + location.pathname; + newlocation = newlocation.replace(/^http/, 'ws'); + if (newlocation.slice(-1) != "/") + newlocation += "/"; + newlocation += "websocket"; + websocket = new WebSocket(newlocation); + + websocket.onmessage = function(e) { on_message(e); }; + + websocket.onopen = function(e) { + const searchParams = (new URL(document.location)).searchParams; + if (!searchParams.has('id')) { + redirect_to_new_page(); + return; + } - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); + var board = document.getElementById("board"); + board.addEventListener("input", function() {on_input(); }); + // Need this workaround (different from direct on_selectionchange) for Chrome. + // Otherwise, callback will not be called on Chrome. + document.addEventListener("selectionchange", handleSelection); + //board.addEventListener("selectionchange", function() {on_selectionchange(); }); + + websocket.send("<request><command>getfile</command><id>" + get_id() + "</id></request>"); + }; + + websocket.onclose = function(e) { + alert("Server connection closed."); + }; + + websocket.onerror = function(e) { + alert("Error: Server connection closed."); + }; - //set_status("Please wait while server prepares " + filename + " ..."); - document.getElementById("qrwindow").onclick = function() { hideQRWindow(); } @@ -161,81 +142,20 @@ function get_id() return searchParams.get('id'); } +// from html function on_new_page() { - redirect_to_new_page(); + redirect_to_new_page(); } function redirect_to_new_page() { - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - var id = this.responseText; - //alert("location=" + document.location.href); - var new_location = document.location.href; - var pos = new_location.search("\\?"); - if (pos >= 0) - new_location = new_location.substring(0, pos); - new_location += '?id=' + id; - - window.location.href = new_location; - //set_status(""); // OK - } - - var parser = new DOMParser(); - var xmlDocument = parser.parseFromString("<request></request>", "text/xml"); - - var requestElement = xmlDocument.getElementsByTagName("request")[0]; - - var commandElement = xmlDocument.createElement("command"); - commandElement.appendChild(document.createTextNode("newid")); - requestElement.appendChild(commandElement); - - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.send(xmlDocument); - - //set_status("Please wait while server prepares " + filename + " ..."); + websocket.send("<request><command>newid</command></request>"); } // local change done -function on_modify() +function on_input() { - timer.reset(); - - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - //set_status(""); // OK - } - var parser = new DOMParser(); var xmlDocument = parser.parseFromString("<request></request>", "text/xml"); @@ -250,131 +170,47 @@ function on_modify() requestElement.appendChild(idElement); var dataElement = xmlDocument.createElement("data"); - dataElement.appendChild(document.createTextNode(addPos(document.getElementById("board").value, document.getElementById("board").selectionStart))); + dataElement.appendChild(document.createTextNode(document.getElementById("board").value)); requestElement.appendChild(dataElement); - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); - - //set_status("Please wait while server prepares " + filename + " ..."); -} - -// checksum of string -function checksum32(s) { - var result = 0; - for (var i = 0; i < s.length; i++) { - result = ((((result >>> 1) | ((result & 1) << 31)) | 0) ^ (s.charCodeAt(i) & 0xFF)) | 0; - } - return (result & 0x7FFFFFFF) | 0; + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } -function textAreaSetPos(id, pos) +// for cursor position +function on_selectionchange(pos) { - document.getElementById(id).selectionStart = pos; - document.getElementById(id).selectionEnd = pos; -} - -function addPos(s, pos) -{ - return s.substr(0, pos) + '\x01' + s.substr(pos); -} - -// gets called by regular polling -function checkupdate() { - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - // no change if response is text/plain - if (this.getResponseHeader("Content-Type") == "application/octet-stream") { - timer.reset(); - var file = new Blob([this.response]); - reader = new FileReader(); - reader.onload = function() { - var board = document.getElementById("board"); - var pos = reader.result.indexOf('\x01'); - if (pos == -1) { // not found - board.value = reader.result; - } else { - board.value = reader.result.substr(0, pos) + reader.result.substr(pos + 1); - } - textAreaSetPos("board", pos); - } - - reader.readAsBinaryString(file); - } - - //set_status(""); // OK - } - var parser = new DOMParser(); var xmlDocument = parser.parseFromString("<request></request>", "text/xml"); var requestElement = xmlDocument.getElementsByTagName("request")[0]; var commandElement = xmlDocument.createElement("command"); - commandElement.appendChild(document.createTextNode("checkupdate")); + commandElement.appendChild(document.createTextNode("cursorpos")); requestElement.appendChild(commandElement); var idElement = xmlDocument.createElement("id"); idElement.appendChild(document.createTextNode(get_id())); requestElement.appendChild(idElement); - var checksumElement = xmlDocument.createElement("checksum"); - checksumElement.appendChild(document.createTextNode(checksum32(addPos(document.getElementById("board").value, document.getElementById("board").selectionStart)))); - requestElement.appendChild(checksumElement); - - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); + var dataElement = xmlDocument.createElement("pos"); + dataElement.appendChild(document.createTextNode(pos)); + requestElement.appendChild(dataElement); - //set_status("Please wait while server prepares " + filename + " ..."); + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } -function on_qrcode() +function textAreaSetPos(id, pos) { - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - if (this.getResponseHeader("Content-Type") == "image/png") { - var blob = new Blob([this.response], {type: 'image/png'}); - var url = URL.createObjectURL(blob); - var img = document.getElementById("qrcode"); - img.src = url; - showQRWindow(); - } - - //set_status(""); // OK + if (document.getElementById(id).selectionStart != pos) { + document.getElementById(id).selectionStart = pos; + document.getElementById(id).selectionEnd = pos; + caretpos = pos; } +} +// HTML button +function on_qrcode() +{ var parser = new DOMParser(); var xmlDocument = parser.parseFromString("<request></request>", "text/xml"); @@ -388,9 +224,6 @@ function on_qrcode() idElement.appendChild(document.createTextNode(document.location)); requestElement.appendChild(idElement); - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } diff --git a/tests/Makefile b/tests/Makefile index 778fd37..15e4106 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -11,7 +11,7 @@ CXXFLAGS+=--coverage LDFLAGS+=--coverage endif -UNITS=storage.cpp config.cpp compiledsql.cpp qrcode.cpp whiteboard.cpp +UNITS=storage.cpp config.cpp compiledsql.cpp qrcode.cpp whiteboard.cpp connectionregistry.cpp UNITTESTS=test-config.cpp \ test-storage.cpp \ @@ -49,6 +49,9 @@ unittests: libgmock.a $(UNITTESTS:.cpp=.o) $(UNITS:.cpp=.o) config.o: ../config.cpp $(CXX) $(CXXFLAGS) -o $@ -c $< +connectionregistry.o: ../connectionregistry.cpp + $(CXX) $(CXXFLAGS) -o $@ -c $< + storage.o: ../storage.cpp $(CXX) $(CXXFLAGS) -o $@ -c $< diff --git a/whiteboard.conf b/whiteboard.conf index 126bef5..055e7ba 100644 --- a/whiteboard.conf +++ b/whiteboard.conf @@ -9,7 +9,7 @@ port: socket to listen on for websocket Example: ::1:8765 --> - <port>::1:8765</port> + <port>::1:9014</port> <!-- Maximum age of individual whiteboard pages in seconds. diff --git a/whiteboard.cpp b/whiteboard.cpp index 6782385..b15ebbe 100644 --- a/whiteboard.cpp +++ b/whiteboard.cpp @@ -93,7 +93,30 @@ std::string make_xml(const std::initializer_list<std::pair<std::string, std::str return oss.str(); } -std::string Whiteboard::handle_request(const std::string& request) +void Whiteboard::notify_other_connections(Whiteboard::connection& c, const std::string& id) +{ + std::for_each(m_registry.begin(id), m_registry.end(id), [&](const Whiteboard::connection& ci) + { + if (c != ci) { + boost::beast::flat_buffer buffer; + boost::beast::ostream(buffer) << make_xml({ + {"type", "getfile"}, + {"data", m_storage->getDocument(id)}, + {"revision", std::to_string(m_storage->getRevision(id)) }, + {"cursorpos", std::to_string(m_storage->getCursorPos(id)) } + }); + std::lock_guard<std::mutex> lock(m_websocket_mutex); + try { + ci->write(buffer.data()); + } catch (const std::exception& ex) { + std::cerr << "Warning: Notify write for " << ci << " not possible, id " << id << std::endl; + m_registry.dump(); + } + } + }); +} + +std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::string& request) { try { std::lock_guard<std::mutex> lock(m_storage_mutex); @@ -109,27 +132,41 @@ std::string Whiteboard::handle_request(const std::string& request) if (command == "modify") { std::string id {xml.get<std::string>("request.id")}; std::string data {xml.get<std::string>("request.data")}; - m_storage->setDocument(id, data); - return make_xml({{"type", "modify"}, {"revision", std::to_string(m_storage->getRevision(id)) }}); + if (m_storage->getDocument(id) != data) { + m_storage->setDocument(id, data); + m_registry.setId(c, id); + notify_other_connections(c, id); + return make_xml({{"type", "modify"}, {"revision", std::to_string(m_storage->getRevision(id)) }}); + } + return {}; + } else if (command == "cursorpos") { + std::string id {xml.get<std::string>("request.id")}; + int pos {xml.get<int>("request.pos")}; + if (m_storage->getCursorPos(id) != pos) { + m_storage->setCursorPos(id, pos); + notify_other_connections(c, id); + } + return {}; } else if (command == "getfile") { std::string id {xml.get<std::string>("request.id")}; - std::string filedata {m_storage->getDocument(id)}; + std::string filedata; + try { + filedata = m_storage->getDocument(id); + } catch (const std::runtime_error&) { + m_storage->setDocument(id, filedata); + } if (filedata.size() > 30000000) throw std::runtime_error("File too big"); - - return make_xml({{"type", "getfile"}, {"data", filedata}, {"revision", std::to_string(m_storage->getRevision(id)) }}); - } else if (command == "checkupdate") { - std::string id {xml.get<std::string>("request.id")}; - int request_revision {xml.get<int>("request.revision")}; - - int revision {m_storage->getRevision(id)}; - if (revision != request_revision) { - return make_xml({{"type", "update"}, {"data", m_storage->getDocument(id)}, {"revision", std::to_string(revision) }}); - } else { - return {}; // no reply - } + m_registry.setId(c, id); + + return make_xml({ + {"type", "getfile"}, + {"data", filedata}, + {"revision", std::to_string(m_storage->getRevision(id)) }, + {"cursorpos", std::to_string(m_storage->getCursorPos(id)) } + }); } else if (command == "newid") { return make_xml({{"type", "newid"}, {"id", m_storage->generate_id()}}); } else if (command == "qrcode") { @@ -146,7 +183,7 @@ std::string Whiteboard::handle_request(const std::string& request) } } catch (const std::exception& ex) { - return "Message handling error: "s + ex.what(); + return make_xml({{"type", "error"}, {"message", "Message handling error: "s + ex.what()}}); } } @@ -154,10 +191,11 @@ void Whiteboard::do_session(boost::asio::ip::tcp::socket socket) { try { // Construct the stream by moving in the socket - boost::beast::websocket::stream<boost::asio::ip::tcp::socket> ws{std::move(socket)}; + std::shared_ptr ws{std::make_shared<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>(std::move(socket))}; + ConnectionRegistry::RegistryGuard guard(m_registry, ws); // Set a decorator to change the Server of the handshake - ws.set_option(boost::beast::websocket::stream_base::decorator( + ws->set_option(boost::beast::websocket::stream_base::decorator( [](boost::beast::websocket::response_type& res) { res.set(boost::beast::http::field::server, @@ -168,27 +206,31 @@ void Whiteboard::do_session(boost::asio::ip::tcp::socket socket) boost::beast::http::request<boost::beast::http::string_body> req; boost::beast::flat_buffer buffer; - boost::beast::http::read(ws.next_layer(), buffer, parser); + boost::beast::http::read(ws->next_layer(), buffer, parser); req = parser.get(); - ws.accept(req); + ws->accept(req); while (true) { boost::beast::flat_buffer buffer; - ws.read(buffer); + ws->read(buffer); - ws.text(ws.got_text()); + ws->text(ws->got_text()); std::string data(boost::asio::buffers_begin(buffer.data()), boost::asio::buffers_end(buffer.data())); - data = handle_request(data); - buffer.consume(buffer.size()); - boost::beast::ostream(buffer) << data; - if (buffer.data().size() > 0) - ws.write(buffer.data()); + data = handle_request(ws, data); + if (buffer.data().size() > 0) { + buffer.consume(buffer.size()); + } + if (data.size() > 0) { + boost::beast::ostream(buffer) << data; + std::lock_guard<std::mutex> lock(m_websocket_mutex); + ws->write(buffer.data()); + } } } catch (boost::beast::system_error const& se) { // This indicates that the session was closed - if (se.code() != boost::beast::websocket::error::closed) + if (se.code() != boost::beast::websocket::error::closed && se.code() != boost::asio::error::eof) std::cerr << "Boost system_error in session: " << se.code().message() << std::endl; } catch (std::exception const& ex) { std::cerr << "Error in session: " << ex.what() << std::endl; diff --git a/whiteboard.h b/whiteboard.h index d645115..e39b94e 100644 --- a/whiteboard.h +++ b/whiteboard.h @@ -8,6 +8,7 @@ #include <boost/asio/ip/tcp.hpp> #include "config.h" +#include "connectionregistry.h" #include "storage.h" class Whiteboard @@ -20,8 +21,13 @@ private: std::unique_ptr<Config> m_config; std::unique_ptr<Storage> m_storage; std::mutex m_storage_mutex; + std::mutex m_websocket_mutex; + + ConnectionRegistry m_registry; - std::string handle_request(const std::string& request); + using connection = std::shared_ptr<boost::beast::websocket::stream<boost::asio::ip::tcp::socket>>; + std::string handle_request(connection& c, const std::string& request); + void notify_other_connections(connection& c, const std::string& id); // notify all other id-related connections about changes void do_session(boost::asio::ip::tcp::socket socket); void storage_cleanup(); }; |