From 8640b8c28213fb35ebecaa57db27c23fa9c67aad Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Fri, 30 Jan 2026 20:25:09 +1100 Subject: [PATCH] BE: in-app notifications overwrite prevention + device huristics update Signed-off-by: jokob-sk --- back/device_heuristics_rules.json | 5 +- server/messaging/in_app.py | 105 ++++++++++++++++++------------ 2 files changed, 67 insertions(+), 43 deletions(-) diff --git a/back/device_heuristics_rules.json b/back/device_heuristics_rules.json index bd14df81..30e2ae5f 100755 --- a/back/device_heuristics_rules.json +++ b/back/device_heuristics_rules.json @@ -155,9 +155,10 @@ "matching_pattern": [ { "mac_prefix": "001FA7", "vendor": "Sony" }, { "mac_prefix": "7C04D0", "vendor": "Nintendo" }, - { "mac_prefix": "EC26CA", "vendor": "Sony" } + { "mac_prefix": "EC26CA", "vendor": "Sony" }, + { "mac_prefix": "48B02D", "vendor": "NVIDIA" } ], - "name_pattern": ["playstation", "xbox"] + "name_pattern": ["playstation", "xbox", "shield", "nvidia"] }, { "dev_type": "Camera", diff --git a/server/messaging/in_app.py b/server/messaging/in_app.py index fc47afdf..ca90962a 100755 --- a/server/messaging/in_app.py +++ b/server/messaging/in_app.py @@ -1,9 +1,9 @@ import os import sys -import _io import json import uuid import time +import fcntl from flask import jsonify @@ -20,6 +20,35 @@ from api_server.sse_broadcast import broadcast_unread_notifications_count # noq NOTIFICATION_API_FILE = apiPath + 'user_notifications.json' +def locked_notifications_file(callback): + # Ensure file exists + if not os.path.exists(NOTIFICATION_API_FILE): + with open(NOTIFICATION_API_FILE, "w") as f: + f.write("[]") + + with open(NOTIFICATION_API_FILE, "r+") as f: + fcntl.flock(f, fcntl.LOCK_EX) + try: + raw = f.read().strip() or "[]" + try: + data = json.loads(raw) + except json.JSONDecodeError: + mylog("none", "[Notification] Corrupted JSON detected, resetting.") + data = [] + + # Let caller modify data + result = callback(data) + + # Write back atomically + f.seek(0) + f.truncate() + json.dump(data, f, indent=4) + + return result + finally: + fcntl.flock(f, fcntl.LOCK_UN) + + # Show Frontend User Notification def write_notification(content, level="alert", timestamp=None): """ @@ -37,45 +66,21 @@ def write_notification(content, level="alert", timestamp=None): if timestamp is None: timestamp = timeNowDB() - # Generate GUID - guid = str(uuid.uuid4()) - - # Prepare notification dictionary notification = { "timestamp": str(timestamp), - "guid": guid, + "guid": str(uuid.uuid4()), "read": 0, "level": level, "content": content, } - # If file exists, load existing data, otherwise initialize as empty list - if os.path.exists(NOTIFICATION_API_FILE): - with open(NOTIFICATION_API_FILE, "r") as file: - # Check if the file object is of type _io.TextIOWrapper - if isinstance(file, _io.TextIOWrapper): - file_contents = file.read() # Read file contents - if file_contents == "": - file_contents = "[]" # If file is empty, initialize as empty list + def update(notifications): + notifications.append(notification) - # mylog('debug', ['[Notification] User Notifications file: ', file_contents]) - notifications = json.loads(file_contents) # Parse JSON data - else: - mylog("none", "[Notification] File is not of type _io.TextIOWrapper") - notifications = [] - else: - notifications = [] + locked_notifications_file(update) - # Append new notification - notifications.append(notification) - - # Write updated data back to file - with open(NOTIFICATION_API_FILE, "w") as file: - json.dump(notifications, file, indent=4) - - # Broadcast unread count update try: - unread_count = sum(1 for n in notifications if n.get("read", 0) == 0) + unread_count = sum(1 for n in locked_notifications_file(lambda n: n) if n.get("read", 0) == 0) broadcast_unread_notifications_count(unread_count) except Exception as e: mylog("none", [f"[Notification] Failed to broadcast unread count: {e}"]) @@ -143,24 +148,42 @@ def mark_all_notifications_read(): "error": str (optional) } """ + # If notifications file does not exist, nothing to mark if not os.path.exists(NOTIFICATION_API_FILE): return {"success": True} try: - with open(NOTIFICATION_API_FILE, "r") as f: - notifications = json.load(f) - except Exception as e: - mylog("none", f"[Notification] Failed to read notifications: {e}") - return {"success": False, "error": str(e)} + # Open file in read/write mode and acquire exclusive lock + with open(NOTIFICATION_API_FILE, "r+") as f: + fcntl.flock(f, fcntl.LOCK_EX) - for n in notifications: - n["read"] = 1 + try: + # Read file contents + file_contents = f.read().strip() + if file_contents == "": + notifications = [] + else: + try: + notifications = json.loads(file_contents) + except json.JSONDecodeError as e: + mylog("none", f"[Notification] Corrupted notifications JSON: {e}") + notifications = [] + + # Mark all notifications as read + for n in notifications: + n["read"] = 1 + + # Rewrite file safely + f.seek(0) + f.truncate() + json.dump(notifications, f, indent=4) + + finally: + # Always release file lock + fcntl.flock(f, fcntl.LOCK_UN) - try: - with open(NOTIFICATION_API_FILE, "w") as f: - json.dump(notifications, f, indent=4) except Exception as e: - mylog("none", f"[Notification] Failed to write notifications: {e}") + mylog("none", f"[Notification] Failed to read/write notifications: {e}") return {"success": False, "error": str(e)} mylog("debug", "[Notification] All notifications marked as read.")