mmrelay 1.1.3__py3-none-any.whl → 1.2.0__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/e2ee_utils.py ADDED
@@ -0,0 +1,392 @@
1
+ """
2
+ Centralized E2EE (End-to-End Encryption) utilities for consistent status detection and messaging.
3
+
4
+ This module provides a unified approach to E2EE status detection, warning messages, and room
5
+ formatting across all components of the meshtastic-matrix-relay application.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from typing import Any, Dict, List, Literal, Optional, TypedDict
11
+
12
+ from mmrelay.cli_utils import get_command
13
+ from mmrelay.constants.app import (
14
+ CREDENTIALS_FILENAME,
15
+ PACKAGE_NAME_E2E,
16
+ PYTHON_OLM_PACKAGE,
17
+ WINDOWS_PLATFORM,
18
+ )
19
+
20
+
21
+ class E2EEStatus(TypedDict):
22
+ """Type definition for E2EE status dictionary."""
23
+
24
+ enabled: bool
25
+ available: bool
26
+ configured: bool
27
+ platform_supported: bool
28
+ dependencies_installed: bool
29
+ credentials_available: bool
30
+ overall_status: Literal["ready", "disabled", "unavailable", "incomplete", "unknown"]
31
+ issues: List[str]
32
+
33
+
34
+ def get_e2ee_status(
35
+ config: Dict[str, Any], config_path: Optional[str] = None
36
+ ) -> E2EEStatus:
37
+ """
38
+ Return a consolidated E2EE status summary by inspecting the runtime platform, required crypto dependencies, configuration, and presence of Matrix credentials.
39
+
40
+ This inspects:
41
+ - platform support (disables on Windows/msys/cygwin),
42
+ - presence of Python olm/nio components,
43
+ - whether E2EE is enabled in the provided config (supports legacy `matrix.encryption.enabled`),
44
+ - whether Matrix credentials (credentials.json) can be found (uses config_path directory if provided, otherwise falls back to the application's base directory).
45
+
46
+ Parameters:
47
+ config (Dict[str, Any]): Parsed application configuration; used to read `matrix.e2ee.enabled` (and legacy `matrix.encryption.enabled`).
48
+ config_path (Optional[str]): Optional path to the configuration file directory to prioritize when checking for credentials.json.
49
+
50
+ Returns:
51
+ E2EEStatus: A dict with the following keys:
52
+ - enabled (bool): E2EE enabled in configuration.
53
+ - available (bool): Platform + dependencies allow E2EE.
54
+ - configured (bool): Authentication/credentials are present.
55
+ - platform_supported (bool): True unless running on Windows/msys/cygwin.
56
+ - dependencies_installed (bool): True if required olm/nio components are importable.
57
+ - credentials_available (bool): True if credentials.json is discovered.
58
+ - overall_status (str): One of "ready", "disabled", "unavailable", "incomplete", or "unknown".
59
+ - issues (List[str]): Human-readable issues found that prevent full E2EE readiness.
60
+ """
61
+ status: E2EEStatus = {
62
+ "enabled": False,
63
+ "available": False,
64
+ "configured": False,
65
+ "platform_supported": True,
66
+ "dependencies_installed": False,
67
+ "credentials_available": False,
68
+ "overall_status": "unknown",
69
+ "issues": [],
70
+ }
71
+
72
+ # Check platform support
73
+ if sys.platform == WINDOWS_PLATFORM or sys.platform.startswith(("msys", "cygwin")):
74
+ status["platform_supported"] = False
75
+ status["issues"].append("E2EE is not supported on Windows")
76
+
77
+ # Check dependencies
78
+ try:
79
+ import olm # noqa: F401
80
+ from nio.crypto import OlmDevice # noqa: F401
81
+ from nio.store import SqliteStore # noqa: F401
82
+
83
+ status["dependencies_installed"] = True
84
+ except ImportError:
85
+ status["dependencies_installed"] = False
86
+ status["issues"].append(
87
+ f"E2EE dependencies not installed ({PYTHON_OLM_PACKAGE})"
88
+ )
89
+
90
+ # Check configuration
91
+ matrix_section = config.get("matrix", {})
92
+ e2ee_config = matrix_section.get("e2ee", {})
93
+ encryption_config = matrix_section.get("encryption", {}) # Legacy support
94
+ status["enabled"] = e2ee_config.get("enabled", False) or encryption_config.get(
95
+ "enabled", False
96
+ )
97
+
98
+ if not status["enabled"]:
99
+ status["issues"].append("E2EE is disabled in configuration")
100
+
101
+ # Check credentials
102
+ if config_path:
103
+ status["credentials_available"] = _check_credentials_available(config_path)
104
+ else:
105
+ # Fallback to base directory check only
106
+ from mmrelay.config import get_base_dir
107
+
108
+ base_credentials_path = os.path.join(get_base_dir(), CREDENTIALS_FILENAME)
109
+ status["credentials_available"] = os.path.exists(base_credentials_path)
110
+
111
+ if not status["credentials_available"]:
112
+ status["issues"].append("Matrix authentication not configured")
113
+
114
+ # Determine overall availability and status
115
+ status["available"] = (
116
+ status["platform_supported"] and status["dependencies_installed"]
117
+ )
118
+ status["configured"] = status["credentials_available"]
119
+
120
+ # Determine overall status
121
+ if not status["platform_supported"]:
122
+ status["overall_status"] = "unavailable"
123
+ elif status["enabled"] and status["available"] and status["configured"]:
124
+ status["overall_status"] = "ready"
125
+ elif not status["enabled"]:
126
+ status["overall_status"] = "disabled"
127
+ else:
128
+ status["overall_status"] = "incomplete"
129
+
130
+ return status
131
+
132
+
133
+ def _check_credentials_available(config_path: str) -> bool:
134
+ """
135
+ Check whether the Matrix credentials file exists in standard locations.
136
+
137
+ Searches for CREDENTIALS_FILENAME in the directory containing the provided configuration file first, then falls back to the application's base directory (via mmrelay.config.get_base_dir()). If the base directory cannot be resolved (ImportError or OSError), the function returns False.
138
+
139
+ Parameters:
140
+ config_path (str): Filesystem path to the configuration file whose directory should be checked.
141
+
142
+ Returns:
143
+ bool: True if the credentials file exists in either the config directory or the base directory; otherwise False.
144
+ """
145
+ # Check config directory first
146
+ config_dir = os.path.dirname(config_path)
147
+ config_credentials_path = os.path.join(config_dir, CREDENTIALS_FILENAME)
148
+
149
+ if os.path.exists(config_credentials_path):
150
+ return True
151
+
152
+ # Fallback to base directory
153
+ try:
154
+ from mmrelay.config import get_base_dir
155
+
156
+ base_credentials_path = os.path.join(get_base_dir(), CREDENTIALS_FILENAME)
157
+ return os.path.exists(base_credentials_path)
158
+ except (ImportError, OSError):
159
+ # If we can't determine base directory, assume no credentials
160
+ return False
161
+
162
+
163
+ def get_room_encryption_warnings(
164
+ rooms: Dict[str, Any], e2ee_status: Dict[str, Any]
165
+ ) -> List[str]:
166
+ """
167
+ Return user-facing warnings for encrypted rooms when E2EE is not fully ready.
168
+
169
+ If the provided E2EE status has overall_status == "ready", returns an empty list.
170
+ Scans the given rooms mapping for items whose `encrypted` attribute is truthy and
171
+ produces one or two warning lines per situation:
172
+ - A line noting how many encrypted rooms were detected and the reason (platform unsupported,
173
+ disabled, or incomplete).
174
+ - A follow-up line indicating whether messages to those rooms will be blocked or may be blocked.
175
+
176
+ Parameters:
177
+ rooms: Mapping of room_id -> room object. Room objects are expected to expose
178
+ an `encrypted` attribute and optionally a `display_name` attribute; room_id is
179
+ used as a fallback name.
180
+ e2ee_status: E2EE status dictionary as returned by get_e2ee_status(); this function
181
+ reads the `overall_status` key to decide warning text.
182
+
183
+ Returns:
184
+ List[str]: Formatted warning lines (empty if no relevant warnings).
185
+ """
186
+ warnings = []
187
+
188
+ if e2ee_status["overall_status"] == "ready":
189
+ # No warnings needed when E2EE is fully ready
190
+ return warnings
191
+
192
+ # Check for encrypted rooms
193
+ encrypted_rooms = []
194
+
195
+ # Handle invalid rooms input
196
+ if not rooms or not hasattr(rooms, "items"):
197
+ return warnings
198
+
199
+ for room_id, room in rooms.items():
200
+ if getattr(room, "encrypted", False):
201
+ room_name = getattr(room, "display_name", room_id)
202
+ encrypted_rooms.append(room_name)
203
+
204
+ if encrypted_rooms:
205
+ overall = e2ee_status["overall_status"]
206
+ if overall == "unavailable":
207
+ warnings.append(
208
+ f"⚠️ {len(encrypted_rooms)} encrypted room(s) detected but E2EE is not supported on Windows"
209
+ )
210
+ elif overall == "disabled":
211
+ warnings.append(
212
+ f"⚠️ {len(encrypted_rooms)} encrypted room(s) detected but E2EE is disabled"
213
+ )
214
+ else:
215
+ warnings.append(
216
+ f"⚠️ {len(encrypted_rooms)} encrypted room(s) detected but E2EE setup is incomplete"
217
+ )
218
+
219
+ # Tail message depends on readiness
220
+ if overall == "incomplete":
221
+ warnings.append(" Messages to encrypted rooms may be blocked")
222
+ else:
223
+ warnings.append(" Messages to encrypted rooms will be blocked")
224
+
225
+ return warnings
226
+
227
+
228
+ def format_room_list(rooms: Dict[str, Any], e2ee_status: Dict[str, Any]) -> List[str]:
229
+ """
230
+ Format a list of human-readable room lines with encryption indicators and status-specific warnings.
231
+
232
+ Given a mapping of room_id -> room-like objects, produce one display string per room:
233
+ - If E2EE overall_status == "ready": encrypted rooms are marked "🔒 {name} - Encrypted"; non-encrypted rooms are "✅ {name}".
234
+ - If not ready: encrypted rooms are prefixed with "⚠️" and include a short reason derived from overall_status ("unavailable" -> not supported on Windows, "disabled" -> disabled in config, otherwise "incomplete"); non-encrypted rooms remain "✅ {name}".
235
+
236
+ Parameters:
237
+ rooms: Mapping of room_id to a room-like object. Each room may have attributes:
238
+ - display_name (str): human-friendly name (fallback: room_id)
239
+ - encrypted (bool): whether the room is encrypted (default: False)
240
+ e2ee_status: E2EE status dictionary (as returned by get_e2ee_status()). Only e2ee_status["overall_status"] is used.
241
+
242
+ Returns:
243
+ List[str]: One formatted line per room suitable for user display.
244
+ """
245
+ room_lines = []
246
+
247
+ # Handle invalid rooms input
248
+ if not rooms or not hasattr(rooms, "items"):
249
+ return room_lines
250
+
251
+ for room_id, room in rooms.items():
252
+ room_name = getattr(room, "display_name", room_id)
253
+ encrypted = getattr(room, "encrypted", False)
254
+
255
+ if e2ee_status["overall_status"] == "ready":
256
+ # Show detailed status when E2EE is fully ready
257
+ if encrypted:
258
+ room_lines.append(f" 🔒 {room_name} - Encrypted")
259
+ else:
260
+ room_lines.append(f" ✅ {room_name}")
261
+ else:
262
+ # Show warnings for encrypted rooms when E2EE is not ready
263
+ if encrypted:
264
+ if e2ee_status["overall_status"] == "unavailable":
265
+ room_lines.append(
266
+ f" ⚠️ {room_name} - Encrypted (E2EE not supported on Windows - messages will be blocked)"
267
+ )
268
+ elif e2ee_status["overall_status"] == "disabled":
269
+ room_lines.append(
270
+ f" ⚠️ {room_name} - Encrypted (E2EE disabled - messages will be blocked)"
271
+ )
272
+ else:
273
+ room_lines.append(
274
+ f" ⚠️ {room_name} - Encrypted (E2EE incomplete - messages may be blocked)"
275
+ )
276
+ else:
277
+ room_lines.append(f" ✅ {room_name}")
278
+
279
+ return room_lines
280
+
281
+
282
+ # Standard warning message templates
283
+ def get_e2ee_warning_messages():
284
+ """
285
+ Return a mapping of standard user-facing E2EE warning messages.
286
+
287
+ Each key is a short status identifier and the value is a ready-to-display message. Messages that reference external tooling or packages are rendered with the module's constants and CLI commands (e.g. PACKAGE_NAME_E2E and get_command).
288
+ Returns:
289
+ dict: Mapping of status keys to formatted warning strings. Keys include:
290
+ - "unavailable", "disabled", "incomplete", "missing_deps",
291
+ "missing_auth", and "missing_config".
292
+ """
293
+ return {
294
+ "unavailable": "E2EE is not supported on Windows - messages to encrypted rooms will be blocked",
295
+ "disabled": "E2EE is disabled in configuration - messages to encrypted rooms will be blocked",
296
+ "incomplete": "E2EE setup is incomplete - messages to encrypted rooms may be blocked",
297
+ "missing_deps": f"E2EE dependencies not installed - run: pipx install {PACKAGE_NAME_E2E}",
298
+ "missing_auth": f"Matrix authentication not configured - run: {get_command('auth_login')}",
299
+ "missing_config": "E2EE not enabled in configuration - add 'e2ee: enabled: true' under matrix section",
300
+ }
301
+
302
+
303
+ def get_e2ee_error_message(e2ee_status: Dict[str, Any]) -> str:
304
+ """
305
+ Return a single user-facing E2EE error message based on the provided E2EE status.
306
+
307
+ If the status is "ready" this returns an empty string. Otherwise selects one actionable
308
+ message (in priority order) for the first failing condition:
309
+ 1. platform not supported
310
+ 2. E2EE disabled in config
311
+ 3. missing E2EE dependencies
312
+ 4. missing Matrix credentials
313
+ 5. otherwise, E2EE setup incomplete
314
+
315
+ Parameters:
316
+ e2ee_status (dict): Status dictionary produced by get_e2ee_status().
317
+ Expected keys used: "overall_status", "platform_supported", "enabled",
318
+ "dependencies_installed", and "credentials_available".
319
+
320
+ Returns:
321
+ str: A single formatted warning/instruction string, or an empty string when ready.
322
+ """
323
+ if e2ee_status.get("overall_status") == "ready":
324
+ return "" # No error
325
+
326
+ # Get current warning messages
327
+ warning_messages = get_e2ee_warning_messages()
328
+
329
+ # Build error message based on specific issues
330
+ if not e2ee_status.get("platform_supported", True):
331
+ return warning_messages["unavailable"]
332
+ elif not e2ee_status.get("enabled", False):
333
+ return warning_messages["disabled"]
334
+ elif not e2ee_status.get("dependencies_installed", False):
335
+ return warning_messages["missing_deps"]
336
+ elif not e2ee_status.get("credentials_available", False):
337
+ return warning_messages["missing_auth"]
338
+ else:
339
+ return warning_messages["incomplete"]
340
+
341
+
342
+ def get_e2ee_fix_instructions(e2ee_status: Dict[str, Any]) -> List[str]:
343
+ """
344
+ Return ordered, user-facing instructions to resolve E2EE setup problems.
345
+
346
+ If E2EE is already ready, returns a single confirmation line. If the platform is unsupported,
347
+ returns platform-specific guidance. Otherwise returns a numbered sequence of actionable steps
348
+ (as separate list lines) to install required E2EE dependencies, provision Matrix credentials,
349
+ enable E2EE in the configuration, and finally verify the configuration. Command and config
350
+ snippets appear as indented lines in the returned list.
351
+
352
+ Parameters:
353
+ e2ee_status (dict): Status mapping produced by get_e2ee_status(). The function reads the
354
+ following keys to decide which steps to include: "overall_status",
355
+ "platform_supported", "dependencies_installed", "credentials_available", and "enabled".
356
+
357
+ Returns:
358
+ List[str]: Ordered, human-readable instruction lines. Each step is a separate string;
359
+ related commands or configuration snippets are returned as additional indented strings.
360
+ """
361
+ if e2ee_status["overall_status"] == "ready":
362
+ return ["✅ E2EE is fully configured and ready"]
363
+
364
+ instructions = []
365
+
366
+ if not e2ee_status["platform_supported"]:
367
+ instructions.append("❌ E2EE is not supported on Windows")
368
+ instructions.append(" Use Linux or macOS for E2EE support")
369
+ return instructions
370
+
371
+ step = 1
372
+ if not e2ee_status["dependencies_installed"]:
373
+ instructions.append(f"{step}. Install E2EE dependencies:")
374
+ instructions.append(f" pipx install {PACKAGE_NAME_E2E}")
375
+ step += 1
376
+
377
+ if not e2ee_status["credentials_available"]:
378
+ instructions.append(f"{step}. Set up Matrix authentication:")
379
+ instructions.append(f" {get_command('auth_login')}")
380
+ step += 1
381
+
382
+ if not e2ee_status["enabled"]:
383
+ instructions.append(f"{step}. Enable E2EE in configuration:")
384
+ instructions.append(" Edit config.yaml and add under matrix section:")
385
+ instructions.append(" e2ee:")
386
+ instructions.append(" enabled: true")
387
+ step += 1
388
+
389
+ instructions.append(f"{step}. Verify configuration:")
390
+ instructions.append(f" {get_command('check_config')}")
391
+
392
+ return instructions
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()
@@ -33,7 +38,14 @@ _component_debug_configured = False
33
38
 
34
39
  # Component logger mapping for data-driven configuration
35
40
  _COMPONENT_LOGGERS = {
36
- "matrix_nio": ["nio", "nio.client", "nio.http", "nio.crypto"],
41
+ "matrix_nio": [
42
+ "nio",
43
+ "nio.client",
44
+ "nio.http",
45
+ "nio.crypto",
46
+ "nio.responses",
47
+ "nio.rooms",
48
+ ],
37
49
  "bleak": ["bleak", "bleak.backends"],
38
50
  "meshtastic": [
39
51
  "meshtastic",
@@ -46,9 +58,14 @@ _COMPONENT_LOGGERS = {
46
58
 
47
59
  def configure_component_debug_logging():
48
60
  """
49
- Enables debug-level logging for selected external components based on configuration settings.
61
+ Configure log levels for external component loggers based on config["logging"]["debug"].
62
+
63
+ Reads per-component entries under `config["logging"]["debug"]` and applies one of:
64
+ - falsy or missing: silence the component by setting its loggers to CRITICAL+1
65
+ - boolean True: enable DEBUG for the component's loggers
66
+ - string: interpret as a logging level name (case-insensitive); invalid names fall back to DEBUG
50
67
 
51
- This function sets the log level to DEBUG for specific libraries (matrix_nio, bleak, meshtastic) if enabled in the global configuration under `logging.debug`. It ensures that component debug logging is configured only once per application run.
68
+ This function mutates the levels of loggers listed in _COMPONENT_LOGGERS and runs only once per process; no-op if called again or if global `config` is None.
52
69
  """
53
70
  global _component_debug_configured, config
54
71
 
@@ -59,21 +76,43 @@ def configure_component_debug_logging():
59
76
  debug_config = config.get("logging", {}).get("debug", {})
60
77
 
61
78
  for component, loggers in _COMPONENT_LOGGERS.items():
62
- if debug_config.get(component, False):
79
+ component_config = debug_config.get(component)
80
+
81
+ if component_config:
82
+ # Component debug is enabled - check if it's a boolean or a log level
83
+ if isinstance(component_config, bool):
84
+ # Legacy boolean format - default to DEBUG
85
+ log_level = logging.DEBUG
86
+ elif isinstance(component_config, str):
87
+ # String log level format (e.g., "warning", "error", "debug")
88
+ try:
89
+ log_level = getattr(logging, component_config.upper())
90
+ except AttributeError:
91
+ # Invalid log level, fall back to DEBUG
92
+ log_level = logging.DEBUG
93
+ else:
94
+ # Invalid config, fall back to DEBUG
95
+ log_level = logging.DEBUG
96
+
63
97
  for logger_name in loggers:
64
- logging.getLogger(logger_name).setLevel(logging.DEBUG)
98
+ logging.getLogger(logger_name).setLevel(log_level)
99
+ else:
100
+ # Component debug is disabled - completely suppress external library logging
101
+ # Use a level higher than CRITICAL to effectively disable all messages
102
+ for logger_name in loggers:
103
+ logging.getLogger(logger_name).setLevel(logging.CRITICAL + 1)
65
104
 
66
105
  _component_debug_configured = True
67
106
 
68
107
 
69
108
  def get_logger(name):
70
109
  """
71
- Create and configure a logger with console and optional rotating file output, using global configuration and command-line arguments.
110
+ Create and configure a logger with console output (optionally colorized) and optional rotating file logging.
72
111
 
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".
112
+ 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
113
 
75
114
  Parameters:
76
- name (str): The name of the logger to create and configure.
115
+ name (str): The name of the logger to create.
77
116
 
78
117
  Returns:
79
118
  logging.Logger: The configured logger instance.
@@ -88,7 +127,11 @@ def get_logger(name):
88
127
  global config
89
128
  if config is not None and "logging" in config:
90
129
  if "level" in config["logging"]:
91
- log_level = getattr(logging, config["logging"]["level"].upper())
130
+ try:
131
+ log_level = getattr(logging, config["logging"]["level"].upper())
132
+ except AttributeError:
133
+ # Invalid log level, fall back to default
134
+ log_level = logging.INFO
92
135
  # Check if colors should be disabled
93
136
  if "color_enabled" in config["logging"]:
94
137
  color_enabled = config["logging"]["color_enabled"]
@@ -96,6 +139,10 @@ def get_logger(name):
96
139
  logger.setLevel(log_level)
97
140
  logger.propagate = False
98
141
 
142
+ # Check if logger already has handlers to avoid duplicates
143
+ if logger.handlers:
144
+ return logger
145
+
99
146
  # Add handler for console logging (with or without colors)
100
147
  if color_enabled:
101
148
  # Use Rich handler with colors
@@ -121,17 +168,28 @@ def get_logger(name):
121
168
  )
122
169
  logger.addHandler(console_handler)
123
170
 
124
- # Check command line arguments for log file path
125
- args = parse_arguments()
171
+ # Check command line arguments for log file path (only if not in test environment)
172
+ args = None
173
+ try:
174
+ # Only parse arguments if we're not in a test environment
175
+ import os
176
+
177
+ if not os.environ.get("MMRELAY_TESTING"):
178
+ from mmrelay.cli import parse_arguments
179
+
180
+ args = parse_arguments()
181
+ except (SystemExit, ImportError):
182
+ # If argument parsing fails (e.g., in tests), continue without CLI arguments
183
+ pass
126
184
 
127
185
  # Check if file logging is enabled (default to True for better user experience)
128
186
  if (
129
187
  config is not None
130
188
  and config.get("logging", {}).get("log_to_file", True)
131
- or args.logfile
189
+ or (args and args.logfile)
132
190
  ):
133
- # Priority: 1. Command line arg, 2. Config file, 3. Default location (~/.mmrelay/logs)
134
- if args.logfile:
191
+ # Priority: 1. Command line argument, 2. Config file, 3. Default location (~/.mmrelay/logs)
192
+ if args and args.logfile:
135
193
  log_file = args.logfile
136
194
  else:
137
195
  config_log_file = (
@@ -153,15 +211,15 @@ def get_logger(name):
153
211
  os.makedirs(log_dir, exist_ok=True)
154
212
 
155
213
  # Store the log file path for later use
156
- if name == "M<>M Relay":
214
+ if name == APP_DISPLAY_NAME:
157
215
  global log_file_path
158
216
  log_file_path = log_file
159
217
 
160
218
  # Create a file handler for logging
161
219
  try:
162
220
  # Set up size-based log rotation
163
- max_bytes = 10 * 1024 * 1024 # Default 10 MB
164
- backup_count = 1 # Default to 1 backup
221
+ max_bytes = DEFAULT_LOG_SIZE_MB * LOG_SIZE_BYTES_MULTIPLIER
222
+ backup_count = DEFAULT_LOG_BACKUP_COUNT
165
223
 
166
224
  if config is not None and "logging" in config:
167
225
  max_bytes = config["logging"].get("max_log_size", max_bytes)