From f44d36b05e43cabde31aeaba5d25fded140345a1 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Fri, 27 Jan 2023 19:42:08 +0100 Subject: Added diff.cpp --- Makefile | 3 +- common.mk | 8 ++++ connectionregistry.cpp | 2 +- connectionregistry.h | 2 +- debian/changelog | 4 ++ diff.cpp | 125 +++++++++++++++++++++++++++++++++++++++++++++++++ diff.h | 24 ++++++++++ html/index.html | 6 ++- html/whiteboard.css | 13 ++++- html/whiteboard.js | 65 ++++++++++++++++++------- tests/Makefile | 5 +- whiteboard.cpp | 55 +++++++++++++++++++--- whiteboard.h | 3 +- 13 files changed, 283 insertions(+), 32 deletions(-) create mode 100644 diff.cpp create mode 100644 diff.h diff --git a/Makefile b/Makefile index 658e764..449e915 100755 --- a/Makefile +++ b/Makefile @@ -9,11 +9,10 @@ include common.mk PROJECTNAME=whiteboard DISTROS=base debian11 ubuntu2210 -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 connectionregistry.h +HEADERS=config.h qrcode.h storage.h whiteboard.h compiledsql.h connectionregistry.h diff.h SOURCES=$(HEADERS:.h=.cpp) OBJECTS=$(HEADERS:.h=.o) TARGETS=whiteboard diff --git a/common.mk b/common.mk index 1f4c2df..5f4c77c 100644 --- a/common.mk +++ b/common.mk @@ -61,3 +61,11 @@ LIBS+=-lboost_filesystem -lpthread LIBS+=-lSQLiteCpp $(shell pkg-config --libs qrcodegencpp GraphicsMagick++ fmt sqlite3) LIBS+=-lreichwein +SRC_ROOT=$(shell echo $(MAKEFILE_LIST) | tr " " "\n" | grep common.mk | sed -e 's/\([^ ]*\)common.mk/\1/g') +ifeq ($(SRC_ROOT),) +SRC_ROOT=. +endif + +VERSION=$(shell dpkg-parsechangelog --show-field Version --file $(SRC_ROOT)/debian/changelog) +CXXFLAGS+=-DWHITEBOARD_VERSION=\"$(VERSION)\" + diff --git a/connectionregistry.cpp b/connectionregistry.cpp index 1e48a96..11a538b 100644 --- a/connectionregistry.cpp +++ b/connectionregistry.cpp @@ -61,7 +61,7 @@ std::unordered_set::iterator ConnectionRegistry: return m_ids.at(id).end(); } -void ConnectionRegistry::dump() +void ConnectionRegistry::dump() const { std::cout << "Connection Registry:" << std::endl; diff --git a/connectionregistry.h b/connectionregistry.h index 2b14553..cdd30d9 100644 --- a/connectionregistry.h +++ b/connectionregistry.h @@ -30,7 +30,7 @@ public: std::unordered_set::iterator begin(const std::string& id); std::unordered_set::iterator end(const std::string& id); - void dump(); + void dump() const; class RegistryGuard { diff --git a/debian/changelog b/debian/changelog index 8373568..2c990d1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,10 @@ whiteboard (1.5) UNRELEASED; urgency=medium * Move from FCGI to websocket interface + * Position changes w/o file transmit + * Print version on main page + * Add reconnect button + * Add diff handling -- Roland Reichwein Sat, 21 Jan 2023 18:18:37 +0100 diff --git a/diff.cpp b/diff.cpp new file mode 100644 index 0000000..b3ed5ce --- /dev/null +++ b/diff.cpp @@ -0,0 +1,125 @@ +#include "diff.h" + +#include +#include + +#include + +namespace pt = boost::property_tree; + +Diff::Diff() +{ +} + +Diff::Diff(const std::string& old_version, const std::string& new_version) +{ + create(old_version, new_version); +} + +std::string Diff::apply(const std::string& old_version) const +{ + std::string result{old_version}; + + result.erase(m_pos0, m_pos1 - m_pos0); + + result.insert(m_pos0, m_data); + + return result; +} + +void Diff::create(const std::string& old_version, const std::string& new_version) +{ + auto front_mismatch{std::mismatch(old_version.cbegin(), old_version.cend(), new_version.cbegin(), new_version.cend())}; + std::string::difference_type old_pos0 {front_mismatch.first - old_version.cbegin()}; + auto& new_pos0 {old_pos0}; + + // equal + if (old_pos0 == old_version.size() && new_pos0 == new_version.size()) { + m_pos0 = 0; + m_pos1 = 0; + m_data.clear(); + return; + } + + // append at end + if (old_pos0 == old_version.size()) { + m_pos0 = old_pos0; + m_pos1 = old_pos0; + m_data = new_version.substr(new_pos0); + return; + } + + // remove from end + if (new_pos0 == new_version.size()) { + m_pos0 = old_pos0; + m_pos1 = old_version.size(); + m_data.clear(); + return; + } + + auto back_mismatch{std::mismatch(old_version.crbegin(), old_version.crend(), new_version.crbegin(), new_version.crend())}; + // i.e. the indices starting from which we can assume equality + size_t old_pos1 {old_version.size() - (back_mismatch.first - old_version.crbegin())}; + size_t new_pos1 {new_version.size() - (back_mismatch.second - new_version.crbegin())}; + + // complete equality is already handled above + + // insert at start + if (old_pos1 == 0) { + m_pos0 = 0; + m_pos1 = 0; + m_data = new_version.substr(0, new_pos1); + return; + } + + // remove from start + if (new_pos1 == 0) { + m_pos0 = 0; + m_pos1 = old_pos1; + m_data.clear(); + return; + } + + // insert in the middle + if (old_pos0 == old_pos1) { + m_pos0 = old_pos0; + m_pos1 = old_pos0; + m_data = new_version.substr(new_pos0, new_pos1 - new_pos0); + return; + } + + // remove from the middle + if (new_pos0 == new_pos1) { + m_pos0 = old_pos0; + m_pos1 = old_pos1; + m_data.clear(); + return; + } + + // last resort: remove and add in the middle + m_pos0 = old_pos0; + m_pos1 = old_pos1; + m_data = new_version.substr(old_pos0, new_pos1 - new_pos0); +} + +boost::property_tree::ptree Diff::get_structure() const +{ + pt::ptree ptree; + ptree.put("diff.chunk.start", std::to_string(m_pos0)); + ptree.put("diff.chunk.end", std::to_string(m_pos1)); + ptree.put("diff.chunk.data", m_data); + + return ptree; +} + +std::string Diff::get_xml() const +{ + pt::ptree ptree{get_structure()}; + + std::ostringstream oss; + // write_xml_element instead of write_xml to omit header + //pt::xml_parser::write_xml(oss, xml); + pt::xml_parser::write_xml_element(oss, {}, ptree, -1, boost::property_tree::xml_writer_settings{}); + return oss.str(); +} + diff --git a/diff.h b/diff.h new file mode 100644 index 0000000..0193238 --- /dev/null +++ b/diff.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +class Diff +{ +public: + Diff(); + Diff(const std::string& old_version, const std::string& new_version); + + std::string apply(const std::string& old_version) const; + void create(const std::string& old_version, const std::string& new_version); + + boost::property_tree::ptree get_structure() const; + std::string get_xml() const; + +private: + // diff replaces space from m_pos0 (inclusive) to m_pos1 (exclusive) with m_data + size_t m_pos0{}; + size_t m_pos1{}; + std::string m_data; +}; diff --git a/html/index.html b/html/index.html index f06ea01..4d1fb2a 100644 --- a/html/index.html +++ b/html/index.html @@ -19,10 +19,12 @@

- + + Connecting... +

- Reichwein.IT Whiteboard by https://www.reichwein.it
+ Reichwein.IT Whiteboard by https://www.reichwein.it
diff --git a/html/whiteboard.css b/html/whiteboard.css index 55e68cf..4de9b46 100644 --- a/html/whiteboard.css +++ b/html/whiteboard.css @@ -1,5 +1,5 @@ body { - font-family: "sans-serif"; + font-family: sans-serif; } figcaption { @@ -100,6 +100,17 @@ img.banner { cursor: pointer; } +.buttonred { + color:#FFFFFF; + background-color:#B05050; + text-decoration: none; + padding: 15px 20px; + font-size: 16px; + border: none; + border-radius: 6px; + cursor: pointer; +} + @media only screen and (min-width: 1px) and (max-width: 630px) { .qrwindow { diff --git a/html/whiteboard.js b/html/whiteboard.js index 9610468..83601b8 100644 --- a/html/whiteboard.js +++ b/html/whiteboard.js @@ -30,8 +30,13 @@ function on_getfile(data, rev, pos) if (board.value != data) { board.value = data; } - textAreaSetPos("board", pos); revision = rev; + textAreaSetPos("board", pos); +} + +function on_getpos(pos) +{ + textAreaSetPos("board", pos); } function on_newid(id) @@ -42,13 +47,17 @@ function on_newid(id) function on_qrcode(png) { - var blob = new Blob([png], {type: 'image/png'}); - var url = URL.createObjectURL(blob); + var url = "data:image/png;base64," + png; var img = document.getElementById("qrcode"); img.src = url; showQRWindow(); } +function on_version(version) +{ + document.getElementById("version").textContent = version; +} + function on_modify_ack(rev) { revision = rev; @@ -63,13 +72,17 @@ function on_message(e) { if (type == "getfile") { on_getfile(xmlDocument.getElementsByTagName("data")[0].textContent, parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent), - parseInt(xmlDocument.getElementsByTagName("cursorpos")[0].textContent)); + parseInt(xmlDocument.getElementsByTagName("pos")[0].textContent)); + } else if (type == "getpos") { + on_getpos(parseInt(xmlDocument.getElementsByTagName("pos")[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 == "version") { + on_version(xmlDocument.getElementsByTagName("version")[0].textContent); } else if (type == "error") { alert(xmlDocument.getElementsByTagName("message")[0].textContent); } else { @@ -88,12 +101,15 @@ function handleSelection() { } } -function init_board() { +function connect_websocket() { + document.getElementById("reconnect").style.display = 'none'; + document.getElementById("connecting").style.display = 'block'; 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); }; @@ -105,24 +121,37 @@ function init_board() { return; } - 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("getversion"); websocket.send("getfile" + get_id() + ""); + document.getElementById("connecting").style.display = 'none'; }; websocket.onclose = function(e) { alert("Server connection closed."); + document.getElementById("reconnect").style.display = 'inline'; }; websocket.onerror = function(e) { alert("Error: Server connection closed."); + document.getElementById("reconnect").style.display = 'inline'; }; +} + +function on_reconnect_click() { + connect_websocket(); +} + +function init_board() { + connect_websocket(); + + 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(); }); + document.getElementById("qrwindow").onclick = function() { hideQRWindow(); } @@ -173,6 +202,10 @@ function on_input() dataElement.appendChild(document.createTextNode(document.getElementById("board").value)); requestElement.appendChild(dataElement); + var posElement = xmlDocument.createElement("pos"); + posElement.appendChild(document.createTextNode(document.getElementById("board").selectionStart)); + requestElement.appendChild(posElement); + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } @@ -192,9 +225,9 @@ function on_selectionchange(pos) idElement.appendChild(document.createTextNode(get_id())); requestElement.appendChild(idElement); - var dataElement = xmlDocument.createElement("pos"); - dataElement.appendChild(document.createTextNode(pos)); - requestElement.appendChild(dataElement); + var posElement = xmlDocument.createElement("pos"); + posElement.appendChild(document.createTextNode(pos)); + requestElement.appendChild(posElement); websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } @@ -209,7 +242,7 @@ function textAreaSetPos(id, pos) } // HTML button -function on_qrcode() +function on_qrcode_click() { var parser = new DOMParser(); var xmlDocument = parser.parseFromString("", "text/xml"); diff --git a/tests/Makefile b/tests/Makefile index 15e4106..c4109d5 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 connectionregistry.cpp +UNITS=storage.cpp config.cpp compiledsql.cpp qrcode.cpp whiteboard.cpp connectionregistry.cpp diff.cpp UNITTESTS=test-config.cpp \ test-storage.cpp \ @@ -52,6 +52,9 @@ config.o: ../config.cpp connectionregistry.o: ../connectionregistry.cpp $(CXX) $(CXXFLAGS) -o $@ -c $< +diff.o: ../diff.cpp + $(CXX) $(CXXFLAGS) -o $@ -c $< + storage.o: ../storage.cpp $(CXX) $(CXXFLAGS) -o $@ -c $< diff --git a/whiteboard.cpp b/whiteboard.cpp index b15ebbe..df18242 100644 --- a/whiteboard.cpp +++ b/whiteboard.cpp @@ -34,6 +34,7 @@ #include +#include "libreichwein/base64.h" #include "libreichwein/file.h" #include "config.h" @@ -89,11 +90,12 @@ std::string make_xml(const std::initializer_list header + //pt::xml_parser::write_xml(oss, xml); pt::xml_parser::write_xml_element(oss, {}, xml, -1, boost::property_tree::xml_writer_settings{}); return oss.str(); } -void Whiteboard::notify_other_connections(Whiteboard::connection& c, const std::string& id) +void Whiteboard::notify_other_connections_file(Whiteboard::connection& c, const std::string& id) { std::for_each(m_registry.begin(id), m_registry.end(id), [&](const Whiteboard::connection& ci) { @@ -103,13 +105,34 @@ void Whiteboard::notify_other_connections(Whiteboard::connection& c, const std:: {"type", "getfile"}, {"data", m_storage->getDocument(id)}, {"revision", std::to_string(m_storage->getRevision(id)) }, - {"cursorpos", std::to_string(m_storage->getCursorPos(id)) } + {"pos", std::to_string(m_storage->getCursorPos(id)) } }); std::lock_guard 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; + std::cerr << "Warning: Notify getfile write for " << ci << " not possible, id " << id << std::endl; + m_registry.dump(); + } + } + }); +} + +void Whiteboard::notify_other_connections_pos(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", "getpos"}, + {"pos", std::to_string(m_storage->getCursorPos(id)) } + }); + std::lock_guard lock(m_websocket_mutex); + try { + ci->write(buffer.data()); + } catch (const std::exception& ex) { + std::cerr << "Warning: Notify getpos write for " << ci << " not possible, id " << id << std::endl; m_registry.dump(); } } @@ -135,7 +158,13 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str if (m_storage->getDocument(id) != data) { m_storage->setDocument(id, data); m_registry.setId(c, id); - notify_other_connections(c, id); + notify_other_connections_file(c, id); + + int pos {xml.get("request.pos")}; + if (m_storage->getCursorPos(id) != pos) { + m_storage->setCursorPos(id, pos); + notify_other_connections_pos(c, id); + } return make_xml({{"type", "modify"}, {"revision", std::to_string(m_storage->getRevision(id)) }}); } return {}; @@ -144,7 +173,7 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str int pos {xml.get("request.pos")}; if (m_storage->getCursorPos(id) != pos) { m_storage->setCursorPos(id, pos); - notify_other_connections(c, id); + notify_other_connections_pos(c, id); } return {}; } else if (command == "getfile") { @@ -165,7 +194,14 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str {"type", "getfile"}, {"data", filedata}, {"revision", std::to_string(m_storage->getRevision(id)) }, - {"cursorpos", std::to_string(m_storage->getCursorPos(id)) } + {"pos", std::to_string(m_storage->getCursorPos(id)) } + }); + } else if (command == "getpos") { + std::string id {xml.get("request.id")}; + + return make_xml({ + {"type", "getpos"}, + {"pos", std::to_string(m_storage->getCursorPos(id)) } }); } else if (command == "newid") { return make_xml({{"type", "newid"}, {"id", m_storage->generate_id()}}); @@ -177,7 +213,12 @@ std::string Whiteboard::handle_request(Whiteboard::connection& c, const std::str std::string pngdata {QRCode::getQRCode(url)}; - return make_xml({{"type", "qrcode"}, {"png", pngdata}}); + return make_xml({{"type", "qrcode"}, {"png", Reichwein::Base64::encode64(pngdata)}}); + } else if (command == "getversion") { + return make_xml({ + {"type", "version"}, + {"version", WHITEBOARD_VERSION } + }); } else { throw std::runtime_error("Bad command: "s + command); } diff --git a/whiteboard.h b/whiteboard.h index e39b94e..818fcfe 100644 --- a/whiteboard.h +++ b/whiteboard.h @@ -27,7 +27,8 @@ private: using connection = std::shared_ptr>; 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 notify_other_connections_file(connection& c, const std::string& id); // notify all other id-related connections about changes + void notify_other_connections_pos(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(); }; -- cgit v1.2.3