From 71bd49bd7d670ff14bea752b205f2e04cc172311 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 2 Dec 2025 09:58:31 -0600 Subject: [PATCH 1/5] Refactor PageNode class to improve page and file registration logic - Consolidated page and file scanning methods to return lists of served pages and files. - Improved error handling in file reading operations. - Updated the announce loop to use a more efficient waiting mechanism. - Improved command-line argument handling for log level configuration. --- rns_page_node/main.py | 156 ++++++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 36 deletions(-) diff --git a/rns_page_node/main.py b/rns_page_node/main.py index 43c972d..a1c4a3a 100644 --- a/rns_page_node/main.py +++ b/rns_page_node/main.py @@ -124,9 +124,10 @@ class PageNode: def register_pages(self): """Scan pages directory and register request handlers for all .mu files.""" + pages = self._scan_pages(self.pagespath) + with self._lock: - self.servedpages = [] - self._scan_pages(self.pagespath) + self.servedpages = pages pagespath = Path(self.pagespath) @@ -137,7 +138,7 @@ class PageNode: allow=RNS.Destination.ALLOW_ALL, ) - for full_path in self.servedpages: + for full_path in pages: rel = full_path[len(str(pagespath)) :] if not rel.startswith("/"): rel = "/" + rel @@ -150,13 +151,14 @@ class PageNode: def register_files(self): """Scan files directory and register request handlers for all files.""" + files = self._scan_files(self.filespath) + with self._lock: - self.servedfiles = [] - self._scan_files(self.filespath) + self.servedfiles = files filespath = Path(self.filespath) - for full_path in self.servedfiles: + for full_path in files: rel = full_path[len(str(filespath)) :] if not rel.startswith("/"): rel = "/" + rel @@ -170,23 +172,31 @@ class PageNode: def _scan_pages(self, base): base_path = Path(base) + if not base_path.exists(): + return [] + served = [] for entry in base_path.iterdir(): if entry.name.startswith("."): continue if entry.is_dir(): - self._scan_pages(str(entry)) + served.extend(self._scan_pages(entry)) elif entry.is_file() and not entry.name.endswith(".allowed"): - self.servedpages.append(str(entry)) + served.append(str(entry)) + return served def _scan_files(self, base): base_path = Path(base) + if not base_path.exists(): + return [] + served = [] for entry in base_path.iterdir(): if entry.name.startswith("."): continue if entry.is_dir(): - self._scan_files(str(entry)) + served.extend(self._scan_files(entry)) elif entry.is_file(): - self.servedfiles.append(str(entry)) + served.append(str(entry)) + return served @staticmethod def serve_default_index( @@ -217,16 +227,21 @@ class PageNode: if not str(file_path).startswith(str(pagespath)): return DEFAULT_NOTALLOWED.encode("utf-8") try: - with file_path.open("rb") as _f: - first_line = _f.readline() - is_script = first_line.startswith(b"#!") - except Exception: - is_script = False + with file_path.open("rb") as file_handle: + first_line = file_handle.readline() + is_script = first_line.startswith(b"#!") + if not is_script: + file_handle.seek(0) + return file_handle.read() + except FileNotFoundError: + return DEFAULT_NOTALLOWED.encode("utf-8") + except OSError as err: + RNS.log(f"Error reading page {file_path}: {err}", RNS.LOG_ERROR) + return DEFAULT_NOTALLOWED.encode("utf-8") + if is_script and os.access(str(file_path), os.X_OK): try: - env_map = {} - if "PATH" in os.environ: - env_map["PATH"] = os.environ["PATH"] + env_map = os.environ.copy() if _link_id is not None: env_map["link_id"] = RNS.hexrep(_link_id, delimit=False) if remote_identity is not None: @@ -249,8 +264,18 @@ class PageNode: return result.stdout except Exception as e: RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR) - with file_path.open("rb") as f: - return f.read() + try: + return self._read_file_bytes(file_path) + except FileNotFoundError: + return DEFAULT_NOTALLOWED.encode("utf-8") + except OSError as err: + RNS.log(f"Error reading page fallback {file_path}: {err}", RNS.LOG_ERROR) + return DEFAULT_NOTALLOWED.encode("utf-8") + + @staticmethod + def _read_file_bytes(file_path): + with file_path.open("rb") as file_handle: + return file_handle.read() def serve_file( self, @@ -278,15 +303,33 @@ class PageNode: """Handle new link connections.""" def _announce_loop(self): + interval_seconds = max(self.announce_interval, 0) * 60 try: while not self._stop_event.is_set(): - if time.time() - self.last_announce > self.announce_interval * 60: - if self.name: - self.destination.announce(app_data=self.name.encode("utf-8")) - else: - self.destination.announce() - self.last_announce = time.time() - time.sleep(1) + now = time.time() + if ( + self.last_announce == 0 + or now - self.last_announce >= interval_seconds + ): + try: + if self.name: + self.destination.announce( + app_data=self.name.encode("utf-8"), + ) + else: + self.destination.announce() + self.last_announce = time.time() + except Exception as announce_error: + RNS.log( + f"Error during announce: {announce_error}", RNS.LOG_ERROR, + ) + wait_time = max( + (self.last_announce + interval_seconds) - time.time() + if self.last_announce + else 0, + 1, + ) + self._stop_event.wait(min(wait_time, 60)) except Exception as e: RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR) @@ -296,17 +339,37 @@ class PageNode: now = time.time() if ( self.page_refresh_interval > 0 - and now - self.last_page_refresh > self.page_refresh_interval + and now - self.last_page_refresh >= self.page_refresh_interval ): self.register_pages() - self.last_page_refresh = now + self.last_page_refresh = time.time() if ( self.file_refresh_interval > 0 - and now - self.last_file_refresh > self.file_refresh_interval + and now - self.last_file_refresh >= self.file_refresh_interval ): self.register_files() - self.last_file_refresh = now - time.sleep(1) + self.last_file_refresh = time.time() + + wait_candidates = [] + if self.page_refresh_interval > 0: + wait_candidates.append( + max( + (self.last_page_refresh + self.page_refresh_interval) + - time.time(), + 0.5, + ), + ) + if self.file_refresh_interval > 0: + wait_candidates.append( + max( + (self.last_file_refresh + self.file_refresh_interval) + - time.time(), + 0.5, + ), + ) + + wait_time = min(wait_candidates) if wait_candidates else 1.0 + self._stop_event.wait(min(wait_time, 60)) except Exception as e: RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR) @@ -430,19 +493,40 @@ def main(): files_dir = get_config_value(args.files_dir, str(Path.cwd() / "files"), "files-dir") node_name = get_config_value(args.node_name, None, "node-name") announce_interval = get_config_value( - args.announce_interval, 360, "announce-interval", int, + args.announce_interval, + 360, + "announce-interval", + int, ) identity_dir = get_config_value( - args.identity_dir, str(Path.cwd() / "node-config"), "identity-dir", + args.identity_dir, + str(Path.cwd() / "node-config"), + "identity-dir", ) page_refresh_interval = get_config_value( - args.page_refresh_interval, 0, "page-refresh-interval", int, + args.page_refresh_interval, + 0, + "page-refresh-interval", + int, ) file_refresh_interval = get_config_value( - args.file_refresh_interval, 0, "file-refresh-interval", int, + args.file_refresh_interval, + 0, + "file-refresh-interval", + int, ) log_level = get_config_value(args.log_level, "INFO", "log-level") + # Set RNS log level based on command line argument + log_level_map = { + "CRITICAL": RNS.LOG_CRITICAL, + "ERROR": RNS.LOG_ERROR, + "WARNING": RNS.LOG_WARNING, + "INFO": RNS.LOG_INFO, + "DEBUG": RNS.LOG_DEBUG, + } + RNS.loglevel = log_level_map.get(log_level.upper(), RNS.LOG_INFO) + RNS.Reticulum(configpath) Path(identity_dir).mkdir(parents=True, exist_ok=True) identity_file = Path(identity_dir) / "identity" From 1571b315b2b69a1b7aa147d9483a06fbf6db5995 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 2 Dec 2025 10:06:56 -0600 Subject: [PATCH 2/5] Add docstrings to PageNode methods for improved clarity --- rns_page_node/main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rns_page_node/main.py b/rns_page_node/main.py index a1c4a3a..0a48781 100644 --- a/rns_page_node/main.py +++ b/rns_page_node/main.py @@ -171,6 +171,7 @@ class PageNode: ) def _scan_pages(self, base): + """Return a list of .mu page paths under the given directory.""" base_path = Path(base) if not base_path.exists(): return [] @@ -185,6 +186,7 @@ class PageNode: return served def _scan_files(self, base): + """Return all file paths under the given directory.""" base_path = Path(base) if not base_path.exists(): return [] @@ -274,6 +276,7 @@ class PageNode: @staticmethod def _read_file_bytes(file_path): + """Read a file's bytes and return the contents.""" with file_path.open("rb") as file_handle: return file_handle.read() @@ -303,6 +306,7 @@ class PageNode: """Handle new link connections.""" def _announce_loop(self): + """Periodically announce the node until shutdown is requested.""" interval_seconds = max(self.announce_interval, 0) * 60 try: while not self._stop_event.is_set(): @@ -334,6 +338,7 @@ class PageNode: RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR) def _refresh_loop(self): + """Refresh page and file registrations at configured intervals.""" try: while not self._stop_event.is_set(): now = time.time() @@ -478,7 +483,7 @@ def main(): return arg_value if config_key in config: try: - if value_type == int: + if value_type is int: return int(config[config_key]) return config[config_key] except ValueError: From d4099fb9a21ef667f4b1f818de32ea7df2b4a948 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 2 Dec 2025 10:17:16 -0600 Subject: [PATCH 3/5] Refactor _scan_pages method and enhance file reading logic in PageNode class - Updated docstring for _scan_pages to clarify exclusion of .allowed files. - Improved file reading logic to handle script detection and content retrieval more efficiently. - Refined error handling during the announce process to catch specific exceptions. --- rns_page_node/main.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rns_page_node/main.py b/rns_page_node/main.py index 0a48781..61c348c 100644 --- a/rns_page_node/main.py +++ b/rns_page_node/main.py @@ -171,7 +171,7 @@ class PageNode: ) def _scan_pages(self, base): - """Return a list of .mu page paths under the given directory.""" + """Return a list of page paths under the given directory, excluding .allowed files.""" base_path = Path(base) if not base_path.exists(): return [] @@ -228,13 +228,16 @@ class PageNode: if not str(file_path).startswith(str(pagespath)): return DEFAULT_NOTALLOWED.encode("utf-8") + is_script = False + file_content = None try: with file_path.open("rb") as file_handle: first_line = file_handle.readline() is_script = first_line.startswith(b"#!") + file_handle.seek(0) if not is_script: - file_handle.seek(0) return file_handle.read() + file_content = file_handle.read() except FileNotFoundError: return DEFAULT_NOTALLOWED.encode("utf-8") except OSError as err: @@ -266,6 +269,8 @@ class PageNode: return result.stdout except Exception as e: RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR) + if file_content is not None: + return file_content try: return self._read_file_bytes(file_path) except FileNotFoundError: @@ -323,9 +328,10 @@ class PageNode: else: self.destination.announce() self.last_announce = time.time() - except Exception as announce_error: + except (TypeError, ValueError) as announce_error: RNS.log( - f"Error during announce: {announce_error}", RNS.LOG_ERROR, + f"Error during announce: {announce_error}", + RNS.LOG_ERROR, ) wait_time = max( (self.last_announce + interval_seconds) - time.time() From 4ec44900cfccb8984069c873aa1c771dbee4a354 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 2 Dec 2025 11:02:01 -0600 Subject: [PATCH 4/5] add windows runner test --- .github/workflows/tests.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d57d63..6917aad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,13 +8,18 @@ on: branches: - main +defaults: + run: + shell: bash + jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} permissions: contents: read strategy: matrix: + os: ["ubuntu-latest", "windows-latest"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: @@ -40,5 +45,5 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-logs-python-${{ matrix.python-version }} + name: test-logs-${{ matrix.os }}-${{ matrix.python-version }} path: tests/node.log From ccf954681be99baeda58cae203b2084e1f120ed9 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 2 Dec 2025 11:03:58 -0600 Subject: [PATCH 5/5] Refactor path handling in PageNode class for improved reliability - Updated path resolution for pages and files to use `resolve()` method, ensuring absolute paths are handled correctly. - Enhanced relative path calculation using `relative_to()` to improve robustness against invalid paths. - Adjusted request path formatting to include a leading slash for consistency. --- rns_page_node/main.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/rns_page_node/main.py b/rns_page_node/main.py index 61c348c..c654935 100644 --- a/rns_page_node/main.py +++ b/rns_page_node/main.py @@ -129,7 +129,7 @@ class PageNode: with self._lock: self.servedpages = pages - pagespath = Path(self.pagespath) + pagespath = Path(self.pagespath).resolve() if not (pagespath / "index.mu").is_file(): self.destination.register_request_handler( @@ -139,10 +139,12 @@ class PageNode: ) for full_path in pages: - rel = full_path[len(str(pagespath)) :] - if not rel.startswith("/"): - rel = "/" + rel - request_path = f"/page{rel}" + page_path = Path(full_path).resolve() + try: + rel = page_path.relative_to(pagespath).as_posix() + except ValueError: + continue + request_path = f"/page/{rel}" self.destination.register_request_handler( request_path, response_generator=self.serve_page, @@ -156,13 +158,15 @@ class PageNode: with self._lock: self.servedfiles = files - filespath = Path(self.filespath) + filespath = Path(self.filespath).resolve() for full_path in files: - rel = full_path[len(str(filespath)) :] - if not rel.startswith("/"): - rel = "/" + rel - request_path = f"/file{rel}" + file_path = Path(full_path).resolve() + try: + rel = file_path.relative_to(filespath).as_posix() + except ValueError: + continue + request_path = f"/file/{rel}" self.destination.register_request_handler( request_path, response_generator=self.serve_file,