From 575fb7d77d8f621ab0ffb860902720a330192e26 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 26 May 2026 13:17:46 +0200 Subject: [PATCH 01/10] Prevent LXM persist race in write_to_directory when messages change state within very short time spans --- LXMF/LXMessage.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/LXMF/LXMessage.py b/LXMF/LXMessage.py index c128c00..a42d1dd 100644 --- a/LXMF/LXMessage.py +++ b/LXMF/LXMessage.py @@ -8,6 +8,7 @@ import multiprocessing import LXMF.LXStamper as LXStamper from .LXMF import APP_NAME, compression_support_from_app_data +from threading import Lock class LXMessage: @@ -184,6 +185,7 @@ class LXMessage: self.__delivery_destination = None self.__delivery_callback = None self.__pn_encrypted_data = None + self.__persist_lock = Lock() self.failed_callback = None self.deferred_stamp_generating = False @@ -674,23 +676,24 @@ class LXMessage: file_path = directory_path+"/"+file_name tmp_path = file_path+".tmp."+str(os.getpid() or time.time()) - try: - with open(tmp_path, "wb") as file: - file.write(self.packed_container()) - file.flush() - try: os.fsync(file.fileno()) - except OSError as e: RNS.log(f"Error while waiting for persist fsync for {self}: {e}", RNS.LOG_WARNING) - - os.replace(tmp_path, file_path) - return file_path - - except Exception as e: + with self.__persist_lock: try: - if os.path.exists(tmp_path): os.unlink(tmp_path) - except Exception as e: RNS.log(f"Error while cleaning temporary file {tmp_path} for {self}: {e}", RNS.LOG_ERROR) + with open(tmp_path, "wb") as file: + file.write(self.packed_container()) + file.flush() + try: os.fsync(file.fileno()) + except OSError as e: RNS.log(f"Error while waiting for persist fsync for {self}: {e}", RNS.LOG_WARNING) - RNS.log(f"Error while writing LXMF message to file \"{file_path}\". The contained exception was: {e}", RNS.LOG_ERROR) - return None + os.replace(tmp_path, file_path) + return file_path + + except Exception as e: + try: + if os.path.exists(tmp_path): os.unlink(tmp_path) + except Exception as e: RNS.log(f"Error while cleaning temporary file {tmp_path} for {self}: {e}", RNS.LOG_ERROR) + + RNS.log(f"Error while writing LXMF message to file \"{file_path}\". The contained exception was: {e}", RNS.LOG_ERROR) + return None def as_uri(self, finalise=True): if not self.packed: From 312e0a8ded5877034d9965d763c0b70ca7921f22 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 26 May 2026 13:18:33 +0200 Subject: [PATCH 02/10] Updated version --- LXMF/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LXMF/_version.py b/LXMF/_version.py index 88081a7..5becc17 100644 --- a/LXMF/_version.py +++ b/LXMF/_version.py @@ -1 +1 @@ -__version__ = "0.9.9" +__version__ = "1.0.0" From 044f3d2879af719a189f399b97a5929b0a9bf6f5 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 26 May 2026 23:32:02 +0200 Subject: [PATCH 03/10] Updated versions --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2ed6932..8798705 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,6 @@ setuptools.setup( 'lxmd=LXMF.Utilities.lxmd:main', ] }, - install_requires=["rns>=1.3.0"], + install_requires=["rns>=1.3.2"], python_requires=">=3.7", ) From bf924c739cf0a5c2ceb4af254b79fb60aaf3adce Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Wed, 27 May 2026 12:28:58 +0200 Subject: [PATCH 04/10] Cleanup --- LXMF/LXMF.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/LXMF/LXMF.py b/LXMF/LXMF.py index c128652..d981ae0 100644 --- a/LXMF/LXMF.py +++ b/LXMF/LXMF.py @@ -101,20 +101,6 @@ RENDERER_MICRON = 0x01 RENDERER_MARKDOWN = 0x02 RENDERER_BBCODE = 0x03 -############################################################ -# To be finalized in 1.0.0. A workdoc with open interaction -# through rngit is available for comments and nuancing on: -# -# a8d24177d946de4f1f0a0fe1af9a1338:/page/work.mu`g=reticulum|r=lxmf -# -# Clients that have implemented different reply, reaction -# or comment mechanisms can choose to transitionally parse -# their own specific formats, but are recommended to attempt -# parsing the structure and format defined herein first, -# and fall back to their client-specific structure second. - -# Reaction dict indicies are integers to preserve bandwidth. -# # Clients choose how to handle reaction content, if at all. # While reactions are typically a single unicode emoji or # similar, the exact implementation and sanitization is @@ -123,8 +109,6 @@ RENDERER_BBCODE = 0x03 REACTION_TO = 0x00 # Bytes, full LXMessage.hash REACTION_CONTENT = 0x01 # Bytes, the reaction content in UTF-8 encoding -# Comment dict indicies are integers to preserve bandwidth. -# # Clients choose how to handle messages intended as comments # for other message, if at all. The actual comment content # is carried as the normal LXM content, meaning clients that @@ -133,8 +117,6 @@ REACTION_CONTENT = 0x01 # Bytes, the reaction content in UTF-8 encoding # with the following keys: COMMENT_FOR = 0x00 # Bytes, full LXMessage.hash -# Continuation dict indicies are integers to preserve bandwidth. -# # Clients choose how to handle messages that continue earlier # messages, if at all. The actual continuation content is # carried as the normal LXM content, meaning clients that @@ -142,7 +124,6 @@ COMMENT_FOR = 0x00 # Bytes, full LXMessage.hash # When using the FIELD_CONTINUATION field, the contents is a # dict with the following keys: CONTINUATION_OF = 0x00 # Bytes, full LXMessage.hash -############################################################ # Optional propagation node metadata fields. These # fields may be highly unstable in allocation and @@ -188,8 +169,7 @@ def display_name_from_app_data(app_data=None): return None # Original announce format - else: - return app_data.decode("utf-8") + else: return app_data.decode("utf-8") def stamp_cost_from_app_data(app_data=None): if app_data == None or app_data == b"": return None @@ -238,8 +218,8 @@ def pn_stamp_cost_from_app_data(app_data=None): if pn_announce_data_is_valid(app_data): data = msgpack.unpackb(app_data) return data[5][0] - else: - return None + + else: return None def pn_announce_data_is_valid(data): try: From 5be161cb1e50e99925f20852324dc3c82a3c0cd1 Mon Sep 17 00:00:00 2001 From: Jeremy O'Brien <956a74d715fe9cb2e9da0a19b067b414> Date: Wed, 27 May 2026 22:52:17 -0400 Subject: [PATCH 05/10] Prevent message write race among different processes touching the same message --- LXMF/LXMessage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LXMF/LXMessage.py b/LXMF/LXMessage.py index a42d1dd..95d2310 100644 --- a/LXMF/LXMessage.py +++ b/LXMF/LXMessage.py @@ -674,7 +674,7 @@ class LXMessage: def write_to_directory(self, directory_path): file_name = RNS.hexrep(self.hash, delimit=False) file_path = directory_path+"/"+file_name - tmp_path = file_path+".tmp."+str(os.getpid() or time.time()) + tmp_path = file_path+".tmp."+str(os.getpid() or time.time())+"."+RNS.hexrep(os.urandom(8), delimit=False) with self.__persist_lock: try: From 11b2480223daae4b942359dde5a1180efc7f8976 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 29 May 2026 09:57:51 +0200 Subject: [PATCH 06/10] Updated versions --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8798705..f8c3421 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,6 @@ setuptools.setup( 'lxmd=LXMF.Utilities.lxmd:main', ] }, - install_requires=["rns>=1.3.2"], + install_requires=["rns>=1.3.4"], python_requires=">=3.7", ) From c877efaec1296b5f1b568daf039193a2b1a70cf4 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 29 May 2026 09:58:06 +0200 Subject: [PATCH 07/10] Updated version --- LXMF/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LXMF/_version.py b/LXMF/_version.py index 5becc17..5c4105c 100644 --- a/LXMF/_version.py +++ b/LXMF/_version.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.1" From 20864133a3940f844606d95542ae0e83cf365cd2 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sun, 31 May 2026 22:10:48 +0200 Subject: [PATCH 08/10] Fixed typo --- LXMF/LXMPeer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LXMF/LXMPeer.py b/LXMF/LXMPeer.py index 0dbf8ce..37515e6 100644 --- a/LXMF/LXMPeer.py +++ b/LXMF/LXMPeer.py @@ -404,7 +404,7 @@ class LXMPeer: if response == LXMPeer.ERROR_NO_IDENTITY: if self.link != None: RNS.log("Remote peer indicated that no identification was received, retrying...", RNS.LOG_VERBOSE) - self.link.identify() + self.link.identify(self.router.identity) self.state = LXMPeer.LINK_READY self.sync() return From a29c4a0e17c55a4556b0c772c9d080d479d99ae8 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 1 Jun 2026 00:03:39 +0200 Subject: [PATCH 09/10] Updated version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f8c3421..beb1432 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,6 @@ setuptools.setup( 'lxmd=LXMF.Utilities.lxmd:main', ] }, - install_requires=["rns>=1.3.4"], + install_requires=["rns>=1.3.5"], python_requires=">=3.7", ) From fab12ad9bf9f997797034950f289fe41a79dcf5a Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 1 Jun 2026 14:26:47 +0200 Subject: [PATCH 10/10] Updated build scripts --- Makefile | 5 +++++ setup.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/Makefile b/Makefile index 5fe5f96..cb7f141 100644 --- a/Makefile +++ b/Makefile @@ -26,5 +26,10 @@ build_spkg: remove_symlinks build_sdist create_symlinks release: remove_symlinks build_wheel build_spkg create_symlinks upload: + @echo Ready to publish release over Reticulum + @read VOID + rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/lxmf create $$(python setup.py --getversion):dist --name lxmf + +upload-pip: @echo Uploading to PyPi... twine upload dist/*.whl dist/*.tar.gz diff --git a/setup.py b/setup.py index beb1432..82521ec 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +import sys import setuptools with open("README.md", "r") as fh: @@ -5,6 +6,10 @@ with open("README.md", "r") as fh: exec(open("LXMF/_version.py", "r").read()) +if "--getversion" in sys.argv: + print(__version__, end="") + exit(0) + setuptools.setup( name="lxmf", version=__version__,