diff --git a/mesh_bot.py b/mesh_bot.py index d7ac5bc..d4e1438 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -9,6 +9,7 @@ except ImportError: exit(1) import asyncio +import sys import time # for sleep, get some when you can :) import random from modules.log import * @@ -24,6 +25,16 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n #Auto response to messages message_lower = message.lower() bot_response = "🤖I'm sorry, I'm afraid I can't do that." + + # Manage cmdHistory size to prevent memory bloat + try: + from modules.system import MAX_CMD_HISTORY + max_cmd_history = MAX_CMD_HISTORY + except ImportError: + max_cmd_history = 1000 + + if len(cmdHistory) >= max_cmd_history: + cmdHistory = cmdHistory[-(max_cmd_history-1):] # Command List processes system.trap_list. system.messageTrap() sends any commands to here default_commands = { @@ -1401,11 +1412,18 @@ def onReceive(packet, interface): else: timestamp = datetime.now().strftime("%Y-%m-%d %I:%M:%S%p") - if len(msg_history) < storeFlimit: - msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode)) - else: - msg_history.pop(0) - msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode)) + # Use the safer MAX_MSG_HISTORY limit to prevent unbounded growth + try: + from modules.system import MAX_MSG_HISTORY + max_history = MAX_MSG_HISTORY + except ImportError: + max_history = storeFlimit + + if len(msg_history) >= max_history: + # Remove oldest entries to maintain size limit + msg_history = msg_history[-(max_history-1):] + + msg_history.append((get_name_from_number(message_from_id, 'long', rxNode), message_string, channel_number, timestamp, rxNode)) # print the message to the log and sdout logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\ @@ -1633,18 +1651,44 @@ async def start_rx(): # Hello World async def main(): - meshRxTask = asyncio.create_task(start_rx()) - watchdogTask = asyncio.create_task(watchdog()) - if file_monitor_enabled: - fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher()) - if radio_detection_enabled: - hamlibTask = asyncio.create_task(handleSignalWatcher()) - - await asyncio.gather(meshRxTask, watchdogTask) - if radio_detection_enabled: - await asyncio.gather(hamlibTask) - if file_monitor_enabled: - await asyncio.gather(fileMonTask) + tasks = [] + + try: + # Create core tasks + tasks.append(asyncio.create_task(start_rx(), name="mesh_rx")) + tasks.append(asyncio.create_task(watchdog(), name="watchdog")) + + # Add optional tasks + if file_monitor_enabled: + tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor")) + + if radio_detection_enabled: + tasks.append(asyncio.create_task(handleSignalWatcher(), name="hamlib")) + + logger.info(f"System: Starting {len(tasks)} async tasks") + + # Wait for all tasks with proper exception handling + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Check for exceptions in results + for i, result in enumerate(results): + if isinstance(result, Exception): + logger.error(f"Task {tasks[i].get_name()} failed with: {result}") + + except Exception as e: + logger.error(f"Main loop error: {e}") + finally: + # Cleanup tasks + logger.info("System: Cleaning up async tasks") + for task in tasks: + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + logger.debug(f"Task {task.get_name()} cancelled successfully") + except Exception as e: + logger.warning(f"Error cancelling task {task.get_name()}: {e}") await asyncio.sleep(0.01) diff --git a/modules/bbstools.py b/modules/bbstools.py index d3cfd11..b83ad89 100644 --- a/modules/bbstools.py +++ b/modules/bbstools.py @@ -26,18 +26,27 @@ def load_bbsdb(): # if the message is not a duplicate, add it to bbs_messages Maintain the message ID sequence new_id = len(bbs_messages) + 1 bbs_messages.append([new_id, msg[1], msg[2], msg[3]]) - except Exception as e: + except FileNotFoundError: + logger.debug("System: bbsdb.pkl not found, creating new one") + bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]] + try: + with open('data/bbsdb.pkl', 'wb') as f: + pickle.dump(bbs_messages, f) + except Exception as e: + logger.error(f"System: Error creating bbsdb.pkl: {e}") + except Exception as e: + logger.error(f"System: Error loading bbsdb.pkl: {e}") bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]] - logger.debug("System: Creating new data/bbsdb.pkl") - with open('data/bbsdb.pkl', 'wb') as f: - pickle.dump(bbs_messages, f) def save_bbsdb(): global bbs_messages # save the bbs messages to the database file - logger.debug("System: Saving data/bbsdb.pkl") - with open('data/bbsdb.pkl', 'wb') as f: - pickle.dump(bbs_messages, f) + try: + logger.debug("System: Saving data/bbsdb.pkl") + with open('data/bbsdb.pkl', 'wb') as f: + pickle.dump(bbs_messages, f) + except Exception as e: + logger.error(f"System: Error saving bbsdb: {e}") def bbs_help(): # help message diff --git a/modules/system.py b/modules/system.py index d34728b..1ad117b 100644 --- a/modules/system.py +++ b/modules/system.py @@ -9,6 +9,7 @@ import asyncio import random import contextlib # for suppressing output on watchdog import io # for suppressing output on watchdog +import atexit # for graceful shutdown from modules.log import * # Global Variables @@ -19,6 +20,73 @@ games_enabled = False multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}] interface_retry_count = 3 +# Memory Management Constants +MAX_CMD_HISTORY = 1000 +MAX_SEEN_NODES = 500 +MAX_MSG_HISTORY = 100 +CLEANUP_INTERVAL = 3600 # 1 hour +last_cleanup_time = 0 + +def cleanup_memory(): + """Clean up memory by limiting list sizes and removing stale entries""" + global cmdHistory, seenNodes, last_cleanup_time + current_time = time.time() + + try: + # Limit cmdHistory size + if 'cmdHistory' in globals() and len(cmdHistory) > MAX_CMD_HISTORY: + cmdHistory = cmdHistory[-MAX_CMD_HISTORY:] + logger.debug(f"System: Trimmed cmdHistory to {MAX_CMD_HISTORY} entries") + + # Clean up old seenNodes entries (older than 24 hours) + if 'seenNodes' in globals(): + initial_count = len(seenNodes) + seenNodes = [node for node in seenNodes + if current_time - node.get('lastSeen', 0) < 86400] + if len(seenNodes) < initial_count: + logger.debug(f"System: Cleaned up {initial_count - len(seenNodes)} old seenNodes entries") + + # Clean up stale game tracker entries + cleanup_game_trackers(current_time) + + # Clean up multiPingList of completed or stale entries + if 'multiPingList' in globals(): + multiPingList[:] = [ping for ping in multiPingList + if ping.get('message_from_id', 0) != 0 and + ping.get('count', 0) > 0] + + last_cleanup_time = current_time + + except Exception as e: + logger.error(f"System: Error during memory cleanup: {e}") + +def cleanup_game_trackers(current_time): + """Clean up all game tracker lists of stale entries""" + try: + # List of game tracker global variable names + tracker_names = [ + 'dwPlayerTracker', 'lemonadeTracker', 'jackTracker', + 'vpTracker', 'mindTracker', 'golfTracker', + 'hangmanTracker', 'hamtestTracker' + ] + + for tracker_name in tracker_names: + if tracker_name in globals(): + tracker = globals()[tracker_name] + if isinstance(tracker, list): + initial_count = len(tracker) + # Remove entries older than GAMEDELAY + globals()[tracker_name] = [ + entry for entry in tracker + if current_time - entry.get('last_played', entry.get('time', 0)) < GAMEDELAY + ] + cleaned_count = initial_count - len(globals()[tracker_name]) + if cleaned_count > 0: + logger.debug(f"System: Cleaned up {cleaned_count} stale entries from {tracker_name}") + + except Exception as e: + logger.error(f"System: Error cleaning up game trackers: {e}") + # Ping Configuration if ping_enabled: # ping, pinging, ack, testing, test, pong diff --git a/pong_bot.py b/pong_bot.py index 094cde2..ef282dc 100755 --- a/pong_bot.py +++ b/pong_bot.py @@ -478,14 +478,41 @@ async def start_rx(): # Hello World async def main(): - meshRxTask = asyncio.create_task(start_rx()) - watchdogTask = asyncio.create_task(watchdog()) - if file_monitor_enabled: - fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher()) - - await asyncio.gather(meshRxTask, watchdogTask) - if file_monitor_enabled: - await asyncio.gather(fileMonTask) + tasks = [] + + try: + # Create core tasks + tasks.append(asyncio.create_task(start_rx(), name="pong_rx")) + tasks.append(asyncio.create_task(watchdog(), name="watchdog")) + + # Add optional tasks + if file_monitor_enabled: + tasks.append(asyncio.create_task(handleFileWatcher(), name="file_monitor")) + + logger.info(f"System: Starting {len(tasks)} async tasks") + + # Wait for all tasks with proper exception handling + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Check for exceptions in results + for i, result in enumerate(results): + if isinstance(result, Exception): + logger.error(f"Task {tasks[i].get_name()} failed with: {result}") + + except Exception as e: + logger.error(f"Main loop error: {e}") + finally: + # Cleanup tasks + logger.info("System: Cleaning up async tasks") + for task in tasks: + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + logger.debug(f"Task {task.get_name()} cancelled successfully") + except Exception as e: + logger.warning(f"Error cancelling task {task.get_name()}: {e}") await asyncio.sleep(0.01)