summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRoland Reichwein <mail@reichwein.it>2023-01-26 20:46:30 +0100
committerRoland Reichwein <mail@reichwein.it>2023-01-26 20:46:30 +0100
commit789e5555ab4c44a1ae779eccf6ccf8340602cf22 (patch)
treefd1c15ac38ec4d43965d8e12a149ae52a0808a73
parent004db5e7e4e9ab6ac5b4730873c6b8f58da92930 (diff)
Websockets: Notify other clients of changes
-rwxr-xr-xMakefile4
-rw-r--r--connectionregistry.cpp95
-rw-r--r--connectionregistry.h46
-rw-r--r--debian/whiteboard.conf26
-rw-r--r--html/whiteboard.js409
-rw-r--r--tests/Makefile5
-rw-r--r--whiteboard.conf2
-rw-r--r--whiteboard.cpp100
-rw-r--r--whiteboard.h8
9 files changed, 373 insertions, 322 deletions
diff --git a/Makefile b/Makefile
index cb5ef3e..658e764 100755
--- a/Makefile
+++ b/Makefile
@@ -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();
};