mmrelay 1.1.3__py3-none-any.whl → 1.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mmrelay might be problematic. Click here for more details.

mmrelay/db_utils.py CHANGED
@@ -31,12 +31,9 @@ def clear_db_path_cache():
31
31
  # Get the database path
32
32
  def get_db_path():
33
33
  """
34
- Resolve and return the file path to the SQLite database, using configuration overrides if provided.
34
+ Resolves and returns the file path to the SQLite database, using configuration overrides if provided.
35
35
 
36
- By default, returns the path to `meshtastic.sqlite` in the standard data directory (`~/.mmrelay/data`).
37
- If a custom path is specified in the configuration under `database.path` (preferred) or `db.path` (legacy),
38
- that path is used instead. The resolved path is cached for subsequent calls, and the directory is created
39
- if it does not exist. Cache is automatically invalidated if the relevant configuration changes.
36
+ Prefers the path specified in `config["database"]["path"]`, falls back to `config["db"]["path"]` (legacy), and defaults to `meshtastic.sqlite` in the standard data directory if neither is set. The resolved path is cached and the cache is invalidated if relevant configuration changes. Attempts to create the directory for the database path if it does not exist.
40
37
  """
41
38
  global config, _cached_db_path, _db_path_logged, _cached_config_hash
42
39
 
@@ -69,7 +66,13 @@ def get_db_path():
69
66
  # Ensure the directory exists
70
67
  db_dir = os.path.dirname(custom_path)
71
68
  if db_dir:
72
- os.makedirs(db_dir, exist_ok=True)
69
+ try:
70
+ os.makedirs(db_dir, exist_ok=True)
71
+ except (OSError, PermissionError) as e:
72
+ logger.warning(
73
+ f"Could not create database directory {db_dir}: {e}"
74
+ )
75
+ # Continue anyway - the database connection will fail later if needed
73
76
 
74
77
  # Cache the path and log only once
75
78
  _cached_db_path = custom_path
@@ -104,76 +107,128 @@ def get_db_path():
104
107
 
105
108
  # Initialize SQLite database
106
109
  def initialize_database():
110
+ """
111
+ Initializes the SQLite database schema for the relay application.
112
+
113
+ Creates required tables (`longnames`, `shortnames`, `plugin_data`, and `message_map`) if they do not exist, and ensures the `meshtastic_meshnet` column is present in `message_map`. Raises an exception if database initialization fails.
114
+ """
107
115
  db_path = get_db_path()
108
116
  # Check if database exists
109
117
  if os.path.exists(db_path):
110
118
  logger.info(f"Loading database from: {db_path}")
111
119
  else:
112
120
  logger.info(f"Creating new database at: {db_path}")
113
- with sqlite3.connect(db_path) as conn:
114
- cursor = conn.cursor()
115
- # Updated table schema: matrix_event_id is now PRIMARY KEY, meshtastic_id is not necessarily unique
116
- cursor.execute(
117
- "CREATE TABLE IF NOT EXISTS longnames (meshtastic_id TEXT PRIMARY KEY, longname TEXT)"
118
- )
119
- cursor.execute(
120
- "CREATE TABLE IF NOT EXISTS shortnames (meshtastic_id TEXT PRIMARY KEY, shortname TEXT)"
121
- )
122
- cursor.execute(
123
- "CREATE TABLE IF NOT EXISTS plugin_data (plugin_name TEXT, meshtastic_id TEXT, data TEXT, PRIMARY KEY (plugin_name, meshtastic_id))"
124
- )
125
- # Changed the schema for message_map: matrix_event_id is now primary key
126
- # Added a new column 'meshtastic_meshnet' to store the meshnet origin of the message.
127
- # If table already exists, we try adding the column if it doesn't exist.
128
- cursor.execute(
129
- "CREATE TABLE IF NOT EXISTS message_map (meshtastic_id INTEGER, matrix_event_id TEXT PRIMARY KEY, matrix_room_id TEXT, meshtastic_text TEXT, meshtastic_meshnet TEXT)"
130
- )
131
-
132
- # Attempt to add meshtastic_meshnet column if it's missing (for upgrades)
133
- # This is a no-op if the column already exists.
134
- # If user runs fresh, it will already be there from CREATE TABLE IF NOT EXISTS.
135
- try:
136
- cursor.execute("ALTER TABLE message_map ADD COLUMN meshtastic_meshnet TEXT")
137
- except sqlite3.OperationalError:
138
- # Column already exists, or table just created with it
139
- pass
121
+ try:
122
+ with sqlite3.connect(db_path) as conn:
123
+ cursor = conn.cursor()
124
+ # Updated table schema: matrix_event_id is now PRIMARY KEY, meshtastic_id is not necessarily unique
125
+ cursor.execute(
126
+ "CREATE TABLE IF NOT EXISTS longnames (meshtastic_id TEXT PRIMARY KEY, longname TEXT)"
127
+ )
128
+ cursor.execute(
129
+ "CREATE TABLE IF NOT EXISTS shortnames (meshtastic_id TEXT PRIMARY KEY, shortname TEXT)"
130
+ )
131
+ cursor.execute(
132
+ "CREATE TABLE IF NOT EXISTS plugin_data (plugin_name TEXT, meshtastic_id TEXT, data TEXT, PRIMARY KEY (plugin_name, meshtastic_id))"
133
+ )
134
+ # Changed the schema for message_map: matrix_event_id is now primary key
135
+ # Added a new column 'meshtastic_meshnet' to store the meshnet origin of the message.
136
+ # If table already exists, we try adding the column if it doesn't exist.
137
+ cursor.execute(
138
+ "CREATE TABLE IF NOT EXISTS message_map (meshtastic_id INTEGER, matrix_event_id TEXT PRIMARY KEY, matrix_room_id TEXT, meshtastic_text TEXT, meshtastic_meshnet TEXT)"
139
+ )
140
140
 
141
- conn.commit()
141
+ # Attempt to add meshtastic_meshnet column if it's missing (for upgrades)
142
+ # This is a no-op if the column already exists.
143
+ # If user runs fresh, it will already be there from CREATE TABLE IF NOT EXISTS.
144
+ try:
145
+ cursor.execute(
146
+ "ALTER TABLE message_map ADD COLUMN meshtastic_meshnet TEXT"
147
+ )
148
+ except sqlite3.OperationalError:
149
+ # Column already exists, or table just created with it
150
+ pass
151
+ except sqlite3.Error as e:
152
+ logger.error(f"Database initialization failed: {e}")
153
+ raise
142
154
 
143
155
 
144
156
  def store_plugin_data(plugin_name, meshtastic_id, data):
145
- with sqlite3.connect(get_db_path()) as conn:
146
- cursor = conn.cursor()
147
- cursor.execute(
148
- "INSERT OR REPLACE INTO plugin_data (plugin_name, meshtastic_id, data) VALUES (?, ?, ?) ON CONFLICT (plugin_name, meshtastic_id) DO UPDATE SET data = ?",
149
- (plugin_name, meshtastic_id, json.dumps(data), json.dumps(data)),
157
+ """
158
+ Store or update JSON-serialized plugin data for a specific plugin and Meshtastic ID in the database.
159
+
160
+ Parameters:
161
+ plugin_name (str): The name of the plugin.
162
+ meshtastic_id (str): The Meshtastic node identifier.
163
+ data (Any): The plugin data to be serialized and stored.
164
+ """
165
+ try:
166
+ with sqlite3.connect(get_db_path()) as conn:
167
+ cursor = conn.cursor()
168
+ cursor.execute(
169
+ "INSERT OR REPLACE INTO plugin_data (plugin_name, meshtastic_id, data) VALUES (?, ?, ?) ON CONFLICT (plugin_name, meshtastic_id) DO UPDATE SET data = ?",
170
+ (plugin_name, meshtastic_id, json.dumps(data), json.dumps(data)),
171
+ )
172
+ conn.commit()
173
+ except sqlite3.Error as e:
174
+ logger.error(
175
+ f"Database error storing plugin data for {plugin_name}, {meshtastic_id}: {e}"
150
176
  )
151
- conn.commit()
152
177
 
153
178
 
154
179
  def delete_plugin_data(plugin_name, meshtastic_id):
155
- with sqlite3.connect(get_db_path()) as conn:
156
- cursor = conn.cursor()
157
- cursor.execute(
158
- "DELETE FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?",
159
- (plugin_name, meshtastic_id),
180
+ """
181
+ Deletes the plugin data entry for the specified plugin and Meshtastic ID from the database.
182
+
183
+ Parameters:
184
+ plugin_name (str): The name of the plugin whose data should be deleted.
185
+ meshtastic_id (str): The Meshtastic node ID associated with the plugin data.
186
+ """
187
+ try:
188
+ with sqlite3.connect(get_db_path()) as conn:
189
+ cursor = conn.cursor()
190
+ cursor.execute(
191
+ "DELETE FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?",
192
+ (plugin_name, meshtastic_id),
193
+ )
194
+ conn.commit()
195
+ except sqlite3.Error as e:
196
+ logger.error(
197
+ f"Database error deleting plugin data for {plugin_name}, {meshtastic_id}: {e}"
160
198
  )
161
- conn.commit()
162
199
 
163
200
 
164
201
  # Get the data for a given plugin and Meshtastic ID
165
202
  def get_plugin_data_for_node(plugin_name, meshtastic_id):
166
- with sqlite3.connect(get_db_path()) as conn:
167
- cursor = conn.cursor()
168
- cursor.execute(
169
- "SELECT data FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?",
170
- (
171
- plugin_name,
172
- meshtastic_id,
173
- ),
203
+ """
204
+ Retrieve and decode plugin data for a specific plugin and Meshtastic node.
205
+
206
+ Returns:
207
+ list: The deserialized plugin data as a list, or an empty list if no data is found or on error.
208
+ """
209
+ try:
210
+ with sqlite3.connect(get_db_path()) as conn:
211
+ cursor = conn.cursor()
212
+ cursor.execute(
213
+ "SELECT data FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?",
214
+ (
215
+ plugin_name,
216
+ meshtastic_id,
217
+ ),
218
+ )
219
+ result = cursor.fetchone()
220
+ try:
221
+ return json.loads(result[0] if result else "[]")
222
+ except (json.JSONDecodeError, TypeError) as e:
223
+ logger.error(
224
+ f"Failed to decode JSON data for plugin {plugin_name}, node {meshtastic_id}: {e}"
225
+ )
226
+ return []
227
+ except (MemoryError, sqlite3.Error) as e:
228
+ logger.error(
229
+ f"Database error retrieving plugin data for {plugin_name}, node {meshtastic_id}: {e}"
174
230
  )
175
- result = cursor.fetchone()
176
- return json.loads(result[0] if result else "[]")
231
+ return []
177
232
 
178
233
 
179
234
  # Get the data for a given plugin
@@ -189,26 +244,53 @@ def get_plugin_data(plugin_name):
189
244
 
190
245
  # Get the longname for a given Meshtastic ID
191
246
  def get_longname(meshtastic_id):
192
- with sqlite3.connect(get_db_path()) as conn:
193
- cursor = conn.cursor()
194
- cursor.execute(
195
- "SELECT longname FROM longnames WHERE meshtastic_id=?", (meshtastic_id,)
196
- )
197
- result = cursor.fetchone()
198
- return result[0] if result else None
247
+ """
248
+ Retrieve the long name associated with a given Meshtastic ID.
249
+
250
+ Parameters:
251
+ meshtastic_id (str): The Meshtastic node identifier.
252
+
253
+ Returns:
254
+ str or None: The long name if found, otherwise None.
255
+ """
256
+ try:
257
+ with sqlite3.connect(get_db_path()) as conn:
258
+ cursor = conn.cursor()
259
+ cursor.execute(
260
+ "SELECT longname FROM longnames WHERE meshtastic_id=?", (meshtastic_id,)
261
+ )
262
+ result = cursor.fetchone()
263
+ return result[0] if result else None
264
+ except sqlite3.Error as e:
265
+ logger.error(f"Database error retrieving longname for {meshtastic_id}: {e}")
266
+ return None
199
267
 
200
268
 
201
269
  def save_longname(meshtastic_id, longname):
202
- with sqlite3.connect(get_db_path()) as conn:
203
- cursor = conn.cursor()
204
- cursor.execute(
205
- "INSERT OR REPLACE INTO longnames (meshtastic_id, longname) VALUES (?, ?)",
206
- (meshtastic_id, longname),
207
- )
208
- conn.commit()
270
+ """
271
+ Insert or update the long name for a given Meshtastic ID in the database.
272
+
273
+ If an entry for the Meshtastic ID already exists, its long name is updated; otherwise, a new entry is created.
274
+ """
275
+ try:
276
+ with sqlite3.connect(get_db_path()) as conn:
277
+ cursor = conn.cursor()
278
+ cursor.execute(
279
+ "INSERT OR REPLACE INTO longnames (meshtastic_id, longname) VALUES (?, ?)",
280
+ (meshtastic_id, longname),
281
+ )
282
+ conn.commit()
283
+ except sqlite3.Error as e:
284
+ logger.error(f"Database error saving longname for {meshtastic_id}: {e}")
209
285
 
210
286
 
211
287
  def update_longnames(nodes):
288
+ """
289
+ Updates the long names for all users in the provided nodes dictionary.
290
+
291
+ Parameters:
292
+ nodes (dict): A dictionary of nodes, each containing user information with Meshtastic IDs and long names.
293
+ """
212
294
  if nodes:
213
295
  for node in nodes.values():
214
296
  user = node.get("user")
@@ -219,26 +301,54 @@ def update_longnames(nodes):
219
301
 
220
302
 
221
303
  def get_shortname(meshtastic_id):
222
- with sqlite3.connect(get_db_path()) as conn:
223
- cursor = conn.cursor()
224
- cursor.execute(
225
- "SELECT shortname FROM shortnames WHERE meshtastic_id=?", (meshtastic_id,)
226
- )
227
- result = cursor.fetchone()
228
- return result[0] if result else None
304
+ """
305
+ Retrieve the short name associated with a given Meshtastic ID.
306
+
307
+ Parameters:
308
+ meshtastic_id (str): The Meshtastic node ID to look up.
309
+
310
+ Returns:
311
+ str or None: The short name if found, or None if not found or on database error.
312
+ """
313
+ try:
314
+ with sqlite3.connect(get_db_path()) as conn:
315
+ cursor = conn.cursor()
316
+ cursor.execute(
317
+ "SELECT shortname FROM shortnames WHERE meshtastic_id=?",
318
+ (meshtastic_id,),
319
+ )
320
+ result = cursor.fetchone()
321
+ return result[0] if result else None
322
+ except sqlite3.Error as e:
323
+ logger.error(f"Database error retrieving shortname for {meshtastic_id}: {e}")
324
+ return None
229
325
 
230
326
 
231
327
  def save_shortname(meshtastic_id, shortname):
232
- with sqlite3.connect(get_db_path()) as conn:
233
- cursor = conn.cursor()
234
- cursor.execute(
235
- "INSERT OR REPLACE INTO shortnames (meshtastic_id, shortname) VALUES (?, ?)",
236
- (meshtastic_id, shortname),
237
- )
238
- conn.commit()
328
+ """
329
+ Insert or update the short name for a given Meshtastic ID in the database.
330
+
331
+ If an entry for the Meshtastic ID already exists, its short name is updated; otherwise, a new entry is created.
332
+ """
333
+ try:
334
+ with sqlite3.connect(get_db_path()) as conn:
335
+ cursor = conn.cursor()
336
+ cursor.execute(
337
+ "INSERT OR REPLACE INTO shortnames (meshtastic_id, shortname) VALUES (?, ?)",
338
+ (meshtastic_id, shortname),
339
+ )
340
+ conn.commit()
341
+ except sqlite3.Error as e:
342
+ logger.error(f"Database error saving shortname for {meshtastic_id}: {e}")
239
343
 
240
344
 
241
345
  def update_shortnames(nodes):
346
+ """
347
+ Updates the short names for all users in the provided nodes dictionary.
348
+
349
+ Parameters:
350
+ nodes (dict): A dictionary of nodes, each containing user information with Meshtastic IDs and short names.
351
+ """
242
352
  if nodes:
243
353
  for node in nodes.values():
244
354
  user = node.get("user")
@@ -256,64 +366,103 @@ def store_message_map(
256
366
  meshtastic_meshnet=None,
257
367
  ):
258
368
  """
259
- Stores a message map in the database.
260
-
261
- :param meshtastic_id: The Meshtastic message ID (integer or None)
262
- :param matrix_event_id: The Matrix event ID (string, primary key)
263
- :param matrix_room_id: The Matrix room ID (string)
264
- :param meshtastic_text: The text of the Meshtastic message
265
- :param meshtastic_meshnet: The name of the meshnet this message originated from.
266
- This helps us identify remote vs local mesh origins.
369
+ Stores or updates a mapping between a Meshtastic message and its corresponding Matrix event in the database.
370
+
371
+ Parameters:
372
+ meshtastic_id: The Meshtastic message ID.
373
+ matrix_event_id: The Matrix event ID (primary key).
374
+ matrix_room_id: The Matrix room ID.
375
+ meshtastic_text: The text content of the Meshtastic message.
376
+ meshtastic_meshnet: Optional name of the meshnet where the message originated, used to distinguish remote from local mesh origins.
267
377
  """
268
- with sqlite3.connect(get_db_path()) as conn:
269
- cursor = conn.cursor()
270
- logger.debug(
271
- f"Storing message map: meshtastic_id={meshtastic_id}, matrix_event_id={matrix_event_id}, matrix_room_id={matrix_room_id}, meshtastic_text={meshtastic_text}, meshtastic_meshnet={meshtastic_meshnet}"
272
- )
273
- cursor.execute(
274
- "INSERT OR REPLACE INTO message_map (meshtastic_id, matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) VALUES (?, ?, ?, ?, ?)",
275
- (
276
- meshtastic_id,
277
- matrix_event_id,
278
- matrix_room_id,
279
- meshtastic_text,
280
- meshtastic_meshnet,
281
- ),
282
- )
283
- conn.commit()
378
+ try:
379
+ with sqlite3.connect(get_db_path()) as conn:
380
+ cursor = conn.cursor()
381
+ logger.debug(
382
+ f"Storing message map: meshtastic_id={meshtastic_id}, matrix_event_id={matrix_event_id}, matrix_room_id={matrix_room_id}, meshtastic_text={meshtastic_text}, meshtastic_meshnet={meshtastic_meshnet}"
383
+ )
384
+ cursor.execute(
385
+ "INSERT OR REPLACE INTO message_map (meshtastic_id, matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) VALUES (?, ?, ?, ?, ?)",
386
+ (
387
+ meshtastic_id,
388
+ matrix_event_id,
389
+ matrix_room_id,
390
+ meshtastic_text,
391
+ meshtastic_meshnet,
392
+ ),
393
+ )
394
+ conn.commit()
395
+ except sqlite3.Error as e:
396
+ logger.error(f"Database error storing message map for {matrix_event_id}: {e}")
284
397
 
285
398
 
286
399
  def get_message_map_by_meshtastic_id(meshtastic_id):
287
- with sqlite3.connect(get_db_path()) as conn:
288
- cursor = conn.cursor()
289
- cursor.execute(
290
- "SELECT matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet FROM message_map WHERE meshtastic_id=?",
291
- (meshtastic_id,),
292
- )
293
- result = cursor.fetchone()
294
- logger.debug(
295
- f"Retrieved message map by meshtastic_id={meshtastic_id}: {result}"
400
+ """
401
+ Retrieve the message mapping entry for a given Meshtastic ID.
402
+
403
+ Returns:
404
+ tuple or None: A tuple (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) if found and valid, or None if not found, on malformed data, or if a database error occurs.
405
+ """
406
+ try:
407
+ with sqlite3.connect(get_db_path()) as conn:
408
+ cursor = conn.cursor()
409
+ cursor.execute(
410
+ "SELECT matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet FROM message_map WHERE meshtastic_id=?",
411
+ (meshtastic_id,),
412
+ )
413
+ result = cursor.fetchone()
414
+ logger.debug(
415
+ f"Retrieved message map by meshtastic_id={meshtastic_id}: {result}"
416
+ )
417
+ if result:
418
+ try:
419
+ # result = (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
420
+ return result[0], result[1], result[2], result[3]
421
+ except (IndexError, TypeError) as e:
422
+ logger.error(
423
+ f"Malformed data in message_map for meshtastic_id {meshtastic_id}: {e}"
424
+ )
425
+ return None
426
+ return None
427
+ except sqlite3.Error as e:
428
+ logger.error(
429
+ f"Database error retrieving message map for meshtastic_id {meshtastic_id}: {e}"
296
430
  )
297
- if result:
298
- # result = (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
299
- return result[0], result[1], result[2], result[3]
300
431
  return None
301
432
 
302
433
 
303
434
  def get_message_map_by_matrix_event_id(matrix_event_id):
304
- with sqlite3.connect(get_db_path()) as conn:
305
- cursor = conn.cursor()
306
- cursor.execute(
307
- "SELECT meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet FROM message_map WHERE matrix_event_id=?",
308
- (matrix_event_id,),
309
- )
310
- result = cursor.fetchone()
311
- logger.debug(
312
- f"Retrieved message map by matrix_event_id={matrix_event_id}: {result}"
435
+ """
436
+ Retrieve the message mapping entry for a given Matrix event ID.
437
+
438
+ Returns:
439
+ tuple or None: A tuple (meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) if found, or None if not found or on error.
440
+ """
441
+ try:
442
+ with sqlite3.connect(get_db_path()) as conn:
443
+ cursor = conn.cursor()
444
+ cursor.execute(
445
+ "SELECT meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet FROM message_map WHERE matrix_event_id=?",
446
+ (matrix_event_id,),
447
+ )
448
+ result = cursor.fetchone()
449
+ logger.debug(
450
+ f"Retrieved message map by matrix_event_id={matrix_event_id}: {result}"
451
+ )
452
+ if result:
453
+ try:
454
+ # result = (meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
455
+ return result[0], result[1], result[2], result[3]
456
+ except (IndexError, TypeError) as e:
457
+ logger.error(
458
+ f"Malformed data in message_map for matrix_event_id {matrix_event_id}: {e}"
459
+ )
460
+ return None
461
+ return None
462
+ except (UnicodeDecodeError, sqlite3.Error) as e:
463
+ logger.error(
464
+ f"Database error retrieving message map for matrix_event_id {matrix_event_id}: {e}"
313
465
  )
314
- if result:
315
- # result = (meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
316
- return result[0], result[1], result[2], result[3]
317
466
  return None
318
467
 
319
468
 
mmrelay/log_utils.py CHANGED
@@ -1,12 +1,17 @@
1
1
  import logging
2
- import os
3
2
  from logging.handlers import RotatingFileHandler
4
3
 
5
4
  from rich.console import Console
6
5
  from rich.logging import RichHandler
7
6
 
8
- from mmrelay.cli import parse_arguments
7
+ # Import parse_arguments only when needed to avoid conflicts with pytest
9
8
  from mmrelay.config import get_log_dir
9
+ from mmrelay.constants.app import APP_DISPLAY_NAME
10
+ from mmrelay.constants.messages import (
11
+ DEFAULT_LOG_BACKUP_COUNT,
12
+ DEFAULT_LOG_SIZE_MB,
13
+ LOG_SIZE_BYTES_MULTIPLIER,
14
+ )
10
15
 
11
16
  # Initialize Rich console
12
17
  console = Console()
@@ -68,12 +73,12 @@ def configure_component_debug_logging():
68
73
 
69
74
  def get_logger(name):
70
75
  """
71
- Create and configure a logger with console and optional rotating file output, using global configuration and command-line arguments.
76
+ Create and configure a logger with console output (optionally colorized) and optional rotating file logging.
72
77
 
73
- The logger supports colorized console output via Rich if enabled, and writes logs to a rotating file if configured or requested via command-line arguments. Log file location and rotation parameters are determined by priority: command-line argument, configuration file, or a default directory. The function ensures the log directory exists and stores the log file path globally if the logger name is "M<>M Relay".
78
+ The logger's log level, colorization, and file logging behavior are determined by global configuration and command-line arguments. Log files are rotated by size, and the log directory is created if necessary. If the logger name matches the application display name, the log file path is stored globally for reference.
74
79
 
75
80
  Parameters:
76
- name (str): The name of the logger to create and configure.
81
+ name (str): The name of the logger to create.
77
82
 
78
83
  Returns:
79
84
  logging.Logger: The configured logger instance.
@@ -88,7 +93,11 @@ def get_logger(name):
88
93
  global config
89
94
  if config is not None and "logging" in config:
90
95
  if "level" in config["logging"]:
91
- log_level = getattr(logging, config["logging"]["level"].upper())
96
+ try:
97
+ log_level = getattr(logging, config["logging"]["level"].upper())
98
+ except AttributeError:
99
+ # Invalid log level, fall back to default
100
+ log_level = logging.INFO
92
101
  # Check if colors should be disabled
93
102
  if "color_enabled" in config["logging"]:
94
103
  color_enabled = config["logging"]["color_enabled"]
@@ -96,6 +105,10 @@ def get_logger(name):
96
105
  logger.setLevel(log_level)
97
106
  logger.propagate = False
98
107
 
108
+ # Check if logger already has handlers to avoid duplicates
109
+ if logger.handlers:
110
+ return logger
111
+
99
112
  # Add handler for console logging (with or without colors)
100
113
  if color_enabled:
101
114
  # Use Rich handler with colors
@@ -121,17 +134,28 @@ def get_logger(name):
121
134
  )
122
135
  logger.addHandler(console_handler)
123
136
 
124
- # Check command line arguments for log file path
125
- args = parse_arguments()
137
+ # Check command line arguments for log file path (only if not in test environment)
138
+ args = None
139
+ try:
140
+ # Only parse arguments if we're not in a test environment
141
+ import os
142
+
143
+ if not os.environ.get("MMRELAY_TESTING"):
144
+ from mmrelay.cli import parse_arguments
145
+
146
+ args = parse_arguments()
147
+ except (SystemExit, ImportError):
148
+ # If argument parsing fails (e.g., in tests), continue without CLI arguments
149
+ pass
126
150
 
127
151
  # Check if file logging is enabled (default to True for better user experience)
128
152
  if (
129
153
  config is not None
130
154
  and config.get("logging", {}).get("log_to_file", True)
131
- or args.logfile
155
+ or (args and args.logfile)
132
156
  ):
133
- # Priority: 1. Command line arg, 2. Config file, 3. Default location (~/.mmrelay/logs)
134
- if args.logfile:
157
+ # Priority: 1. Command line argument, 2. Config file, 3. Default location (~/.mmrelay/logs)
158
+ if args and args.logfile:
135
159
  log_file = args.logfile
136
160
  else:
137
161
  config_log_file = (
@@ -153,15 +177,15 @@ def get_logger(name):
153
177
  os.makedirs(log_dir, exist_ok=True)
154
178
 
155
179
  # Store the log file path for later use
156
- if name == "M<>M Relay":
180
+ if name == APP_DISPLAY_NAME:
157
181
  global log_file_path
158
182
  log_file_path = log_file
159
183
 
160
184
  # Create a file handler for logging
161
185
  try:
162
186
  # Set up size-based log rotation
163
- max_bytes = 10 * 1024 * 1024 # Default 10 MB
164
- backup_count = 1 # Default to 1 backup
187
+ max_bytes = DEFAULT_LOG_SIZE_MB * LOG_SIZE_BYTES_MULTIPLIER
188
+ backup_count = DEFAULT_LOG_BACKUP_COUNT
165
189
 
166
190
  if config is not None and "logging" in config:
167
191
  max_bytes = config["logging"].get("max_log_size", max_bytes)
mmrelay/main.py CHANGED
@@ -15,6 +15,7 @@ from nio.events.room_events import RoomMemberEvent
15
15
  # Import version from package
16
16
  # Import meshtastic_utils as a module to set event_loop
17
17
  from mmrelay import __version__, meshtastic_utils
18
+ from mmrelay.constants.app import APP_DISPLAY_NAME, WINDOWS_PLATFORM
18
19
  from mmrelay.db_utils import (
19
20
  initialize_database,
20
21
  update_longnames,
@@ -36,7 +37,7 @@ from mmrelay.message_queue import (
36
37
  from mmrelay.plugin_loader import load_plugins
37
38
 
38
39
  # Initialize logger
39
- logger = get_logger(name="M<>M Relay")
40
+ logger = get_logger(name=APP_DISPLAY_NAME)
40
41
 
41
42
  # Set the logging level for 'nio' to ERROR to suppress warnings
42
43
  logging.getLogger("nio").setLevel(logging.ERROR)
@@ -57,9 +58,9 @@ def print_banner():
57
58
 
58
59
  async def main(config):
59
60
  """
60
- Runs the main asynchronous relay loop, managing the lifecycle and coordination between Meshtastic and Matrix clients.
61
+ Coordinates the main asynchronous relay loop between Meshtastic and Matrix clients.
61
62
 
62
- Initializes the database, loads plugins, starts the message queue, and establishes connections to both Meshtastic and Matrix. Joins configured Matrix rooms, registers event callbacks for message and membership events, and periodically updates node names from the Meshtastic network. Monitors connection health, manages the Matrix sync loop with reconnection and shutdown handling, and ensures graceful shutdown of all components, including optional message map wiping on startup and shutdown if configured.
63
+ Initializes the database, loads plugins, starts the message queue, and establishes connections to both Meshtastic and Matrix. Joins configured Matrix rooms, registers event callbacks for message and membership events, and periodically updates node names from the Meshtastic network. Monitors connection health, manages the Matrix sync loop with reconnection and shutdown handling, and ensures graceful shutdown of all components. Optionally wipes the message map on startup and shutdown if configured.
63
64
  """
64
65
  # Extract Matrix configuration
65
66
  from typing import List
@@ -133,7 +134,7 @@ async def main(config):
133
134
  loop = asyncio.get_running_loop()
134
135
 
135
136
  # Handle signals differently based on the platform
136
- if sys.platform != "win32":
137
+ if sys.platform != WINDOWS_PLATFORM:
137
138
  for sig in (signal.SIGINT, signal.SIGTERM):
138
139
  loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown()))
139
140
  else: