diff options
| author | Roland Reichwein <mail@reichwein.it> | 2023-01-27 19:42:08 +0100 | 
|---|---|---|
| committer | Roland Reichwein <mail@reichwein.it> | 2023-01-27 19:42:08 +0100 | 
| commit | f44d36b05e43cabde31aeaba5d25fded140345a1 (patch) | |
| tree | 1024a76cb1ae671c9445dcc379cb9eddd26922aa | |
| parent | 789e5555ab4c44a1ae779eccf6ccf8340602cf22 (diff) | |
Added diff.cpp
| -rwxr-xr-x | Makefile | 3 | ||||
| -rw-r--r-- | common.mk | 8 | ||||
| -rw-r--r-- | connectionregistry.cpp | 2 | ||||
| -rw-r--r-- | connectionregistry.h | 2 | ||||
| -rw-r--r-- | debian/changelog | 4 | ||||
| -rw-r--r-- | diff.cpp | 125 | ||||
| -rw-r--r-- | diff.h | 24 | ||||
| -rw-r--r-- | html/index.html | 6 | ||||
| -rw-r--r-- | html/whiteboard.css | 13 | ||||
| -rw-r--r-- | html/whiteboard.js | 65 | ||||
| -rw-r--r-- | tests/Makefile | 5 | ||||
| -rw-r--r-- | whiteboard.cpp | 55 | ||||
| -rw-r--r-- | whiteboard.h | 3 | 
13 files changed, 283 insertions, 32 deletions
@@ -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 @@ -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<ConnectionRegistry::connection>::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<connection>::iterator begin(const std::string& id);   std::unordered_set<connection>::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 <mail@reichwein.it>  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 <algorithm> +#include <sstream> + +#include <boost/property_tree/xml_parser.hpp> + +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 <!xml...> header + //pt::xml_parser::write_xml(oss, xml); + pt::xml_parser::write_xml_element(oss, {}, ptree, -1, boost::property_tree::xml_writer_settings<pt::ptree::key_type>{}); + return oss.str(); +} + @@ -0,0 +1,24 @@ +#pragma once + +#include <string> + +#include <boost/property_tree/ptree.hpp> + +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 @@  			<br/>  			<br/>  			<button class="button" onclick="on_new_page();">New page</button> -			<button class="button" onclick="on_qrcode();">QR code</button> +			<button class="button" onclick="on_qrcode_click();">QR code</button> +			<span id="connecting">Connecting...</span> +			<button class="buttonred" id="reconnect" onclick="on_reconnect_click();" hidden>Reconnect</button>  			<br/>  			<br/> -			Reichwein.IT Whiteboard by <a href="https://www.reichwein.it">https://www.reichwein.it</a><br/> +                        Reichwein.IT Whiteboard <span id="version"></span> by <a href="https://www.reichwein.it">https://www.reichwein.it</a><br/>  		</div>  		<a id="download-a" hidden></a> 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("<request><command>getversion</command></request>");  		websocket.send("<request><command>getfile</command><id>" + get_id() + "</id></request>"); +		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("<request></request>", "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 <fmt/core.h> +#include "libreichwein/base64.h"  #include "libreichwein/file.h"  #include "config.h" @@ -89,11 +90,12 @@ std::string make_xml(const std::initializer_list<std::pair<std::string, std::str   std::ostringstream oss;   // write_xml_element instead of write_xml to omit <!xml...> header + //pt::xml_parser::write_xml(oss, xml);   pt::xml_parser::write_xml_element(oss, {}, xml, -1, boost::property_tree::xml_writer_settings<pt::ptree::key_type>{});   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<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; +                  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<std::mutex> 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<int>("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<int>("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<std::string>("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<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 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();  };  | 
