From a3dd82d6be22d5874136688f27f58dd9e67971cf Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch
Date: Thu, 8 May 2025 13:04:22 +0200 Subject: WIP: Acknowledge received commands Event the smallest protocol change has great consequences. Relay events have been reordered to improve forward compatibility. WIP: - xM handling (send) - xA handling (relaySend) - xC: fix connection killing --- LICENSE | 2 +- NEWS | 10 ++++++++++ liberty | 2 +- xA/xA.go | 2 ++ xC.c | 42 ++++++++++++++++++++++++++++++++---------- xC.lxdr | 53 ++++++++++++++++++++++++++--------------------------- xM/main.swift | 2 ++ xP/public/xP.js | 24 ++++++++++++++++++++---- xT/xT.cpp | 10 ++++++++++ xW/xW.cpp | 10 ++++++++++ 10 files changed, 114 insertions(+), 43 deletions(-) diff --git a/LICENSE b/LICENSE index d58be36..69c9c4c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 - 2024, Přemysl Eric Janouch
+Copyright (c) 2014 - 2025, Přemysl Eric Janouch
 
 Permission to use, copy, modify, and/or distribute this software for any
 purpose with or without fee is hereby granted.
diff --git a/NEWS b/NEWS
index 63870bd..713c0aa 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,13 @@
+Unreleased
+
+ * xC: added more characters as nickname delimiters,
+   so that @nick works as a highlight
+
+ * xP: added a network lag indicator to the user interface
+
+ * Bumped relay protocol version
+
+
 2.1.0 (2024-12-19) "Bunnyrific"
 
  * xC: fixed a crash when the channel topic had too many formatting items
diff --git a/liberty b/liberty
index af889b7..b69d3f8 160000
--- a/liberty
+++ b/liberty
@@ -1 +1 @@
-Subproject commit af889b733e81fa40d7a7ff652386585115e186f5
+Subproject commit b69d3f8692b1d34f9b8616e046cdabe0d2fb67c0
diff --git a/xA/xA.go b/xA/xA.go
index 707a280..118394f 100644
--- a/xA/xA.go
+++ b/xA/xA.go
@@ -339,6 +339,8 @@ func relaySend(data RelayCommandData, callback callback) bool {
 	}
 	if callback != nil {
 		commandCallbacks[m.CommandSeq] = callback
+	} else {
+		// TODO(p)
 	}
 	commandSeq++
 
diff --git a/xC.c b/xC.c
index 73ddd12..1ce3d8d 100644
--- a/xC.c
+++ b/xC.c
@@ -1818,6 +1818,7 @@ struct client
 
 	uint32_t event_seq;                 ///< Outgoing message counter
 	bool initialized;                   ///< Initial sync took place
+	bool shutdown;                      ///< Shutting down
 
 	struct poller_fd socket_event;      ///< The socket can be read/written to
 };
@@ -4168,9 +4169,7 @@ relay_send (struct client *c)
 {
 	struct relay_event_message *m = &c->ctx->relay_message;
 	m->event_seq = c->event_seq++;
-
-	// TODO: Also don't try sending anything if half-closed.
-	if (!c->initialized || c->socket_fd == -1)
+	if (!c->initialized || c->shutdown || c->socket_fd == -1)
 		return;
 
 	// liberty has msg_{reader,writer} already, but they use 8-byte lengths.
@@ -4180,7 +4179,10 @@ relay_send (struct client *c)
 	 || (frame_len = c->write_buffer.len - frame_len_pos - 4) > UINT32_MAX)
 	{
 		print_error ("serialization failed, killing client");
-		client_kill (c);
+		// FIXME: This must not be done immediately!
+		//client_kill (c);
+		// TODO: Perhaps set an idle task that collects shutdown clients.
+		c->shutdown = true;
 		return;
 	}
 
@@ -15716,26 +15718,31 @@ client_process_message (struct client *c,
 		return true;
 	}
 
+	bool acknowledge = false;
 	switch (m->data.command)
 	{
 	case RELAY_COMMAND_HELLO:
+		c->initialized = true;
 		if (m->data.hello.version != RELAY_VERSION)
 		{
-			// TODO: This should send back an error message and shut down.
 			log_global_error (c->ctx,
 				"Protocol version mismatch, killing client");
-			return false;
+			relay_prepare_error (c->ctx,
+				m->command_seq, "Protocol version mismatch");
+			relay_send (c);
+
+			c->shutdown = true;
+			return true;
 		}
-		c->initialized = true;
 		client_resync (c);
+		acknowledge = true;
 		break;
 	case RELAY_COMMAND_PING:
-		relay_prepare_response (c->ctx, m->command_seq)
-			->data.command = RELAY_COMMAND_PING;
-		relay_send (c);
+		acknowledge = true;
 		break;
 	case RELAY_COMMAND_ACTIVE:
 		reset_autoaway (c->ctx);
+		acknowledge = true;
 		break;
 	case RELAY_COMMAND_BUFFER_COMPLETE:
 		client_process_buffer_complete (c, m->command_seq, buffer,
@@ -15743,12 +15750,15 @@ client_process_message (struct client *c,
 		break;
 	case RELAY_COMMAND_BUFFER_ACTIVATE:
 		buffer_activate (c->ctx, buffer);
+		acknowledge = true;
 		break;
 	case RELAY_COMMAND_BUFFER_INPUT:
 		client_process_buffer_input (c, buffer, m->data.buffer_input.text.str);
+		acknowledge = true;
 		break;
 	case RELAY_COMMAND_BUFFER_TOGGLE_UNIMPORTANT:
 		buffer_toggle_unimportant (c->ctx, buffer);
+		acknowledge = true;
 		break;
 	case RELAY_COMMAND_BUFFER_LOG:
 		client_process_buffer_log (c, m->command_seq, buffer);
@@ -15758,6 +15768,12 @@ client_process_message (struct client *c,
 		relay_prepare_error (c->ctx, m->command_seq, "Unknown command");
 		relay_send (c);
 	}
+	if (acknowledge)
+	{
+		relay_prepare_response (c->ctx, m->command_seq)
+			->data.command = m->data.command;
+		relay_send (c);
+	}
 	return true;
 }
 
@@ -15851,7 +15867,13 @@ on_client_ready (const struct pollfd *pfd, void *user_data)
 {
 	struct client *c = user_data;
 	if (client_try_read (c) && client_try_write (c))
+	{
 		client_update_poller (c, pfd);
+
+		// There must be something in the write buffer if you request shutdown.
+		if (c->shutdown && !c->write_buffer.len)
+			client_kill (c);
+	}
 }
 
 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/xC.lxdr b/xC.lxdr
index af0f170..eba914f 100644
--- a/xC.lxdr
+++ b/xC.lxdr
@@ -1,7 +1,8 @@
 // Backwards-compatible protocol version.
-const VERSION = 1;
+const VERSION = 2;
 
 // From the frontend to the relay.
+// All commands receive either an Event.RESPONSE, or an Event.ERROR.
 struct CommandMessage {
 	// The command sequence number will be repeated in responses
 	// in the respective fields.
@@ -32,13 +33,10 @@ struct CommandMessage {
 	// XXX: Perhaps this should rather be handled through a /buffer command.
 	case BUFFER_TOGGLE_UNIMPORTANT:
 		string buffer_name;
-	case PING_RESPONSE:
-		u32 event_seq;
-
-	// Only these commands may produce Event.RESPONSE, as below,
-	// but any command may produce an error.
 	case PING:
 		void;
+	case PING_RESPONSE:
+		u32 event_seq;
 	case BUFFER_COMPLETE:
 		string buffer_name;
 		string text;
@@ -52,6 +50,9 @@ struct CommandMessage {
 struct EventMessage {
 	u32 event_seq;
 	union EventData switch (enum Event {
+		ERROR,
+		RESPONSE,
+
 		PING,
 		BUFFER_LINE,
 		BUFFER_UPDATE,
@@ -64,12 +65,28 @@ struct EventMessage {
 		SERVER_UPDATE,
 		SERVER_RENAME,
 		SERVER_REMOVE,
-		ERROR,
-		RESPONSE,
 	} event) {
+	// Restriction: command_seq strictly follows the sequence received
+	// by the relay, across both of these replies.
+	case ERROR:
+		u32 command_seq;
+		string error;
+	case RESPONSE:
+		u32 command_seq;
+		union ResponseData switch (Command command) {
+		case BUFFER_COMPLETE:
+			u32 start;
+			string completions<>;
+		case BUFFER_LOG:
+			// UTF-8, but not guaranteed.
+			u8 log<>;
+		default:
+			// Reception acknowledged.
+			void;
+		} data;
+
 	case PING:
 		void;
-
 	case BUFFER_LINE:
 		string buffer_name;
 		// Whether the line should also be displayed in the active buffer.
@@ -188,23 +205,5 @@ struct EventMessage {
 		string new;
 	case SERVER_REMOVE:
 		string server_name;
-
-	// Restriction: command_seq strictly follows the sequence received
-	// by the relay, across both of these replies.
-	case ERROR:
-		u32 command_seq;
-		string error;
-	case RESPONSE:
-		u32 command_seq;
-		union ResponseData switch (Command command) {
-		case PING:
-			void;
-		case BUFFER_COMPLETE:
-			u32 start;
-			string completions<>;
-		case BUFFER_LOG:
-			// UTF-8, but not guaranteed.
-			u8 log<>;
-		} data;
 	} data;
 };
diff --git a/xM/main.swift b/xM/main.swift
index 48f26c4..1235f04 100644
--- a/xM/main.swift
+++ b/xM/main.swift
@@ -175,6 +175,8 @@ class RelayRPC {
 		let m = RelayCommandMessage(commandSeq: self.commandSeq, data: data)
 		if let callback = callback {
 			self.commandCallbacks[m.commandSeq] = callback
+		} else {
+			// TODO(p): Add an empty callback.
 		}
 
 		var w = RelayWriter()
diff --git a/xP/public/xP.js b/xP/public/xP.js
index 6035db3..bc91bb3 100644
--- a/xP/public/xP.js
+++ b/xP/public/xP.js
@@ -69,14 +69,22 @@ class RelayRPC extends EventTarget {
 		let e = message.data
 		switch (e.event) {
 		case Relay.Event.Error:
-			if (this.promised[e.commandSeq] !== undefined)
-				this.promised[e.commandSeq].reject(e.error)
+			let p = this.promised[e.commandSeq]
+			// TODO(p): Network indicator.
+			if (p === true)
+				break
+			else if (p !== undefined)
+				p.reject(e.error)
 			else
 				console.error(`Unawaited error: ${e.error}`)
 			break
 		case Relay.Event.Response:
-			if (this.promised[e.commandSeq] !== undefined)
-				this.promised[e.commandSeq].resolve(e.data)
+			let p = this.promised[e.commandSeq]
+			// TODO(p): Network indicator.
+			if (p === true)
+				break
+			else if (p !== undefined)
+				p.resolve(e.data)
 			else
 				console.error("Unawaited response")
 			break
@@ -110,6 +118,9 @@ class RelayRPC extends EventTarget {
 
 		this.ws.send(JSON.stringify({commandSeq: seq, data: params}))
 
+		// TODO(p): Network indicator.
+		this.promised[seq] = true
+
 		// Automagically detect if we want a result.
 		let data = undefined
 		const promise = new Promise(
@@ -998,6 +1009,11 @@ let Input = {
 
 	onKeyDown: event => {
 		// TODO: And perhaps on other actions, too.
+		// TODO: Throttle these, for example by remembering when the last
+		// one was sent (or attempted to be sent), then setting a timeout
+		// and bumping that timeout when already present.
+		// Or even just refusing to resend it within a timeframe.
+		// This deserves a function.
 		rpc.send({command: 'Active'})
 
 		let b = buffers.get(bufferCurrent)
diff --git a/xT/xT.cpp b/xT/xT.cpp
index 72f5892..b708b95 100644
--- a/xT/xT.cpp
+++ b/xT/xT.cpp
@@ -179,6 +179,14 @@ beep()
 
 // --- Networking --------------------------------------------------------------
 
+static void
+on_relay_generic_response(
+	std::wstring error, const Relay::ResponseData *response)
+{
+	if (!response)
+		show_error_message(QString::fromStdWString(error));
+}
+
 static void
 relay_send(Relay::CommandData *data, Callback callback = {})
 {
@@ -190,6 +198,8 @@ relay_send(Relay::CommandData *data, Callback callback = {})
 
 	if (callback)
 		g.command_callbacks[m.command_seq] = std::move(callback);
+	else
+		g.command_callbacks[m.command_seq] = on_relay_generic_response;
 
 	auto len = qToBigEndian