mmrelay 1.2.1__py3-none-any.whl → 1.2.2__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/__init__.py +1 -1
- mmrelay/__main__.py +29 -0
- mmrelay/cli.py +452 -50
- mmrelay/cli_utils.py +59 -9
- mmrelay/config.py +198 -71
- mmrelay/constants/app.py +2 -2
- mmrelay/db_utils.py +73 -26
- mmrelay/e2ee_utils.py +6 -3
- mmrelay/log_utils.py +16 -5
- mmrelay/main.py +41 -38
- mmrelay/matrix_utils.py +1069 -293
- mmrelay/meshtastic_utils.py +350 -206
- mmrelay/message_queue.py +22 -23
- mmrelay/plugin_loader.py +634 -205
- mmrelay/plugins/mesh_relay_plugin.py +43 -38
- mmrelay/plugins/weather_plugin.py +11 -12
- mmrelay/runtime_utils.py +35 -0
- mmrelay/setup_utils.py +324 -129
- mmrelay/tools/mmrelay.service +2 -1
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +11 -72
- mmrelay/tools/sample-docker-compose.yaml +12 -58
- mmrelay/tools/sample_config.yaml +1 -1
- mmrelay/windows_utils.py +349 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/METADATA +7 -7
- mmrelay-1.2.2.dist-info/RECORD +48 -0
- mmrelay-1.2.1.dist-info/RECORD +0 -45
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/WHEEL +0 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.2.1.dist-info → mmrelay-1.2.2.dist-info}/top_level.txt +0 -0
mmrelay/cli.py
CHANGED
|
@@ -9,6 +9,8 @@ import shutil
|
|
|
9
9
|
import sys
|
|
10
10
|
from collections.abc import Mapping
|
|
11
11
|
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
12
14
|
# Import version from package
|
|
13
15
|
from mmrelay import __version__
|
|
14
16
|
from mmrelay.cli_utils import (
|
|
@@ -132,6 +134,11 @@ def parse_arguments():
|
|
|
132
134
|
help="Validate configuration file",
|
|
133
135
|
description="Check configuration file syntax and completeness",
|
|
134
136
|
)
|
|
137
|
+
config_subparsers.add_parser(
|
|
138
|
+
"diagnose",
|
|
139
|
+
help="Diagnose configuration system issues",
|
|
140
|
+
description="Test config generation capabilities and troubleshoot platform-specific issues",
|
|
141
|
+
)
|
|
135
142
|
|
|
136
143
|
# AUTH group
|
|
137
144
|
auth_parser = subparsers.add_parser(
|
|
@@ -230,14 +237,16 @@ def print_version():
|
|
|
230
237
|
|
|
231
238
|
def _validate_e2ee_dependencies():
|
|
232
239
|
"""
|
|
233
|
-
Check whether end-to-end encryption (E2EE)
|
|
234
|
-
|
|
235
|
-
Performs a platform check and attempts to import required packages (python-olm, nio.crypto.OlmDevice,
|
|
236
|
-
and nio.store.SqliteStore). Prints a short user-facing status message and guidance.
|
|
237
|
-
|
|
240
|
+
Check whether end-to-end encryption (E2EE) is usable on the current platform.
|
|
241
|
+
|
|
238
242
|
Returns:
|
|
239
|
-
bool: True if the platform
|
|
240
|
-
|
|
243
|
+
bool: True if the platform is supported and required E2EE libraries can be imported;
|
|
244
|
+
False otherwise.
|
|
245
|
+
|
|
246
|
+
Notes:
|
|
247
|
+
- This function performs only local checks (platform and importability) and does not perform
|
|
248
|
+
network I/O.
|
|
249
|
+
- It emits user-facing messages to indicate missing platform support or missing dependencies.
|
|
241
250
|
"""
|
|
242
251
|
if sys.platform == WINDOWS_PLATFORM:
|
|
243
252
|
print("❌ Error: E2EE is not supported on Windows")
|
|
@@ -254,7 +263,8 @@ def _validate_e2ee_dependencies():
|
|
|
254
263
|
print("✅ E2EE dependencies are installed")
|
|
255
264
|
return True
|
|
256
265
|
except ImportError:
|
|
257
|
-
print("❌ Error: E2EE
|
|
266
|
+
print("❌ Error: E2EE dependencies not installed")
|
|
267
|
+
print(" End-to-end encryption features require additional dependencies")
|
|
258
268
|
print(" Install E2EE support: pipx install 'mmrelay[e2e]'")
|
|
259
269
|
return False
|
|
260
270
|
|
|
@@ -770,23 +780,18 @@ def check_config(args=None):
|
|
|
770
780
|
"""
|
|
771
781
|
Validate the application's YAML configuration file and its required sections.
|
|
772
782
|
|
|
773
|
-
Performs these checks:
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
-
|
|
778
|
-
|
|
779
|
-
- Ensures matrix_rooms exists, is a non-empty list, and each room is a dict containing an id.
|
|
780
|
-
- Validates the meshtastic section: requires connection_type and the connection-specific fields (serial_port for serial, host for tcp/network, ble_address for ble). Warns about deprecated connection types.
|
|
781
|
-
- Validates optional meshtastic fields and types (broadcast_enabled, detection_sensor, message_delay >= 2.0, meshnet_name) and reports missing optional settings as guidance.
|
|
782
|
-
- Warns if a deprecated db section is present.
|
|
783
|
-
- Prints a unified E2EE analysis summary on success.
|
|
783
|
+
Performs these checks: locates the first existing config file from get_config_paths(args),
|
|
784
|
+
validates YAML syntax and non-empty content, verifies Matrix authentication (prefers
|
|
785
|
+
credentials.json but accepts access_token or password in the matrix section), performs
|
|
786
|
+
E2EE configuration and dependency checks, validates matrix_rooms and meshtastic sections
|
|
787
|
+
(including connection-specific requirements), checks a set of optional meshtastic settings
|
|
788
|
+
(and value constraints), and warns about deprecated sections.
|
|
784
789
|
|
|
785
790
|
Side effects:
|
|
786
|
-
- Prints errors, warnings, and status messages to stdout.
|
|
791
|
+
- Prints human-readable errors, warnings, and status messages to stdout.
|
|
787
792
|
|
|
788
793
|
Parameters:
|
|
789
|
-
args (argparse.Namespace | None): Parsed CLI arguments
|
|
794
|
+
args (argparse.Namespace | None): Parsed CLI arguments; if None, CLI arguments are parsed internally.
|
|
790
795
|
|
|
791
796
|
Returns:
|
|
792
797
|
bool: True if a configuration file was found and passed all checks; False otherwise.
|
|
@@ -805,7 +810,7 @@ def check_config(args=None):
|
|
|
805
810
|
config_path = path
|
|
806
811
|
print(f"Found configuration file at: {config_path}")
|
|
807
812
|
try:
|
|
808
|
-
with open(config_path, "r") as f:
|
|
813
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
809
814
|
config_content = f.read()
|
|
810
815
|
|
|
811
816
|
# Validate YAML syntax first
|
|
@@ -1035,7 +1040,8 @@ def check_config(args=None):
|
|
|
1035
1040
|
# Special validation for message_delay
|
|
1036
1041
|
if option == "message_delay" and value < 2.0:
|
|
1037
1042
|
print(
|
|
1038
|
-
f"Error: 'message_delay' must be at least 2.0 seconds (firmware limitation), got: {value}"
|
|
1043
|
+
f"Error: 'message_delay' must be at least 2.0 seconds (firmware limitation), got: {value}",
|
|
1044
|
+
file=sys.stderr,
|
|
1039
1045
|
)
|
|
1040
1046
|
return False
|
|
1041
1047
|
else:
|
|
@@ -1049,16 +1055,23 @@ def check_config(args=None):
|
|
|
1049
1055
|
# Check for deprecated db section
|
|
1050
1056
|
if "db" in config:
|
|
1051
1057
|
print(
|
|
1052
|
-
"\nWarning: 'db' section is deprecated. Please use 'database' instead."
|
|
1058
|
+
"\nWarning: 'db' section is deprecated. Please use 'database' instead.",
|
|
1059
|
+
file=sys.stderr,
|
|
1053
1060
|
)
|
|
1054
1061
|
print(
|
|
1055
|
-
"This option still works but may be removed in future versions.\n"
|
|
1062
|
+
"This option still works but may be removed in future versions.\n",
|
|
1063
|
+
file=sys.stderr,
|
|
1056
1064
|
)
|
|
1057
1065
|
|
|
1058
1066
|
print("\n✅ Configuration file is valid!")
|
|
1059
1067
|
return True
|
|
1068
|
+
except (OSError, ValueError, UnicodeDecodeError) as e:
|
|
1069
|
+
print(
|
|
1070
|
+
f"Error checking configuration: {e.__class__.__name__}: {e}",
|
|
1071
|
+
file=sys.stderr,
|
|
1072
|
+
)
|
|
1060
1073
|
except Exception as e:
|
|
1061
|
-
print(f"Error checking configuration: {e}")
|
|
1074
|
+
print(f"Error checking configuration: {e}", file=sys.stderr)
|
|
1062
1075
|
return False
|
|
1063
1076
|
|
|
1064
1077
|
print("Error: No configuration file found in any of the following locations:")
|
|
@@ -1083,8 +1096,27 @@ def main():
|
|
|
1083
1096
|
int: Exit code (0 on success, non-zero on failure).
|
|
1084
1097
|
"""
|
|
1085
1098
|
try:
|
|
1099
|
+
# Set up Windows console for better compatibility
|
|
1100
|
+
try:
|
|
1101
|
+
from mmrelay.windows_utils import setup_windows_console
|
|
1102
|
+
|
|
1103
|
+
setup_windows_console()
|
|
1104
|
+
except (ImportError, OSError, AttributeError):
|
|
1105
|
+
# windows_utils not available or Windows console setup failed
|
|
1106
|
+
# This is intentional - we want to continue if Windows utils fail
|
|
1107
|
+
pass
|
|
1108
|
+
|
|
1086
1109
|
args = parse_arguments()
|
|
1087
1110
|
|
|
1111
|
+
# Handle the --data-dir option
|
|
1112
|
+
if args and args.data_dir:
|
|
1113
|
+
import mmrelay.config
|
|
1114
|
+
|
|
1115
|
+
# Set the global custom_data_dir variable
|
|
1116
|
+
mmrelay.config.custom_data_dir = os.path.abspath(args.data_dir)
|
|
1117
|
+
# Create the directory if it doesn't exist
|
|
1118
|
+
os.makedirs(mmrelay.config.custom_data_dir, exist_ok=True)
|
|
1119
|
+
|
|
1088
1120
|
# Handle subcommands first (modern interface)
|
|
1089
1121
|
if hasattr(args, "command") and args.command:
|
|
1090
1122
|
return handle_subcommand(args)
|
|
@@ -1125,20 +1157,33 @@ def main():
|
|
|
1125
1157
|
print(f"Error importing main module: {e}")
|
|
1126
1158
|
return 1
|
|
1127
1159
|
|
|
1160
|
+
except (OSError, PermissionError, KeyboardInterrupt) as e:
|
|
1161
|
+
# Handle common system-level errors
|
|
1162
|
+
print(f"System error: {e.__class__.__name__}: {e}", file=sys.stderr)
|
|
1163
|
+
return 1
|
|
1128
1164
|
except Exception as e:
|
|
1129
|
-
|
|
1165
|
+
# Default error message
|
|
1166
|
+
error_msg = f"Unexpected error: {e.__class__.__name__}: {e}"
|
|
1167
|
+
# Provide Windows-specific error guidance if available
|
|
1168
|
+
try:
|
|
1169
|
+
from mmrelay.windows_utils import get_windows_error_message, is_windows
|
|
1170
|
+
|
|
1171
|
+
if is_windows():
|
|
1172
|
+
error_msg = f"Error: {get_windows_error_message(e)}"
|
|
1173
|
+
except ImportError:
|
|
1174
|
+
pass # Use default message
|
|
1175
|
+
print(error_msg, file=sys.stderr)
|
|
1130
1176
|
return 1
|
|
1131
1177
|
|
|
1132
1178
|
|
|
1133
1179
|
def handle_subcommand(args):
|
|
1134
1180
|
"""
|
|
1135
|
-
Dispatch the modern grouped CLI subcommand to
|
|
1136
|
-
|
|
1137
|
-
Parameters:
|
|
1138
|
-
args (argparse.Namespace): Parsed CLI arguments (as produced by parse_arguments()). Must have a `command` attribute with one of: "config", "auth", or "service".
|
|
1181
|
+
Dispatch the modern grouped CLI subcommand to the appropriate handler and return an exit code.
|
|
1139
1182
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1183
|
+
The function expects an argparse.Namespace from parse_arguments() with a `command`
|
|
1184
|
+
attribute set to one of: "config", "auth", or "service". Delegates to the
|
|
1185
|
+
corresponding handler and returns its exit code. If `command` is unknown,
|
|
1186
|
+
prints an error and returns 1.
|
|
1142
1187
|
"""
|
|
1143
1188
|
if args.command == "config":
|
|
1144
1189
|
return handle_config_command(args)
|
|
@@ -1146,6 +1191,7 @@ def handle_subcommand(args):
|
|
|
1146
1191
|
return handle_auth_command(args)
|
|
1147
1192
|
elif args.command == "service":
|
|
1148
1193
|
return handle_service_command(args)
|
|
1194
|
+
|
|
1149
1195
|
else:
|
|
1150
1196
|
print(f"Unknown command: {args.command}")
|
|
1151
1197
|
return 1
|
|
@@ -1153,21 +1199,25 @@ def handle_subcommand(args):
|
|
|
1153
1199
|
|
|
1154
1200
|
def handle_config_command(args):
|
|
1155
1201
|
"""
|
|
1156
|
-
Dispatch the
|
|
1202
|
+
Dispatch the "config" command group to the selected subcommand handler.
|
|
1157
1203
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1204
|
+
Supported subcommands:
|
|
1205
|
+
- "generate": create or update the sample configuration file at the preferred location.
|
|
1206
|
+
- "check": validate the resolved configuration file (delegates to check_config).
|
|
1207
|
+
- "diagnose": run a sequence of non-destructive diagnostics and print a report (delegates to handle_config_diagnose).
|
|
1160
1208
|
|
|
1161
1209
|
Parameters:
|
|
1162
|
-
args (argparse.Namespace):
|
|
1210
|
+
args (argparse.Namespace): CLI namespace containing `config_command` (one of "generate", "check", "diagnose") and any subcommand-specific options.
|
|
1163
1211
|
|
|
1164
1212
|
Returns:
|
|
1165
|
-
int:
|
|
1213
|
+
int: Exit code (0 on success, 1 on failure or for unknown subcommands).
|
|
1166
1214
|
"""
|
|
1167
1215
|
if args.config_command == "generate":
|
|
1168
1216
|
return 0 if generate_sample_config() else 1
|
|
1169
1217
|
elif args.config_command == "check":
|
|
1170
1218
|
return 0 if check_config(args) else 1
|
|
1219
|
+
elif args.config_command == "diagnose":
|
|
1220
|
+
return handle_config_diagnose(args)
|
|
1171
1221
|
else:
|
|
1172
1222
|
print(f"Unknown config command: {args.config_command}")
|
|
1173
1223
|
return 1
|
|
@@ -1262,8 +1312,27 @@ def handle_auth_login(args):
|
|
|
1262
1312
|
return 1
|
|
1263
1313
|
else:
|
|
1264
1314
|
# No parameters provided - run in interactive mode
|
|
1265
|
-
|
|
1266
|
-
|
|
1315
|
+
# Check if E2EE is actually configured before mentioning it
|
|
1316
|
+
# Use silent checking to avoid warnings during initial setup
|
|
1317
|
+
try:
|
|
1318
|
+
from mmrelay.config import check_e2ee_enabled_silently
|
|
1319
|
+
|
|
1320
|
+
e2ee_enabled = check_e2ee_enabled_silently(args)
|
|
1321
|
+
|
|
1322
|
+
if e2ee_enabled:
|
|
1323
|
+
print("Matrix Bot Authentication for E2EE")
|
|
1324
|
+
print("===================================")
|
|
1325
|
+
else:
|
|
1326
|
+
print("\nMatrix Bot Authentication")
|
|
1327
|
+
print("=========================")
|
|
1328
|
+
except (OSError, PermissionError, ImportError, ValueError) as e:
|
|
1329
|
+
# Fallback if silent checking fails due to config file or import issues
|
|
1330
|
+
from mmrelay.log_utils import get_logger
|
|
1331
|
+
|
|
1332
|
+
logger = get_logger("CLI")
|
|
1333
|
+
logger.debug(f"Failed to silently check E2EE status: {e}")
|
|
1334
|
+
print("\nMatrix Bot Authentication")
|
|
1335
|
+
print("=========================")
|
|
1267
1336
|
|
|
1268
1337
|
try:
|
|
1269
1338
|
result = asyncio.run(
|
|
@@ -1429,10 +1498,16 @@ def handle_auth_logout(args):
|
|
|
1429
1498
|
|
|
1430
1499
|
def handle_service_command(args):
|
|
1431
1500
|
"""
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
Currently supports the "install" subcommand which
|
|
1435
|
-
Returns 0 on
|
|
1501
|
+
Dispatch service-related subcommands.
|
|
1502
|
+
|
|
1503
|
+
Currently supports the "install" subcommand which imports and runs mmrelay.setup_utils.install_service().
|
|
1504
|
+
Returns 0 on successful installation, 1 on failure or for unknown subcommands.
|
|
1505
|
+
|
|
1506
|
+
Parameters:
|
|
1507
|
+
args: argparse.Namespace with a `service_command` attribute indicating the requested action.
|
|
1508
|
+
|
|
1509
|
+
Returns:
|
|
1510
|
+
int: Exit code (0 on success, 1 on error).
|
|
1436
1511
|
"""
|
|
1437
1512
|
if args.service_command == "install":
|
|
1438
1513
|
try:
|
|
@@ -1447,6 +1522,259 @@ def handle_service_command(args):
|
|
|
1447
1522
|
return 1
|
|
1448
1523
|
|
|
1449
1524
|
|
|
1525
|
+
def _diagnose_config_paths(args):
|
|
1526
|
+
"""
|
|
1527
|
+
Print a diagnostic summary of resolved configuration file search paths and their directory accessibility.
|
|
1528
|
+
|
|
1529
|
+
Uses get_config_paths(args) to compute the ordered list of candidate config file locations, then prints each path with a concise directory status icon:
|
|
1530
|
+
- ✅ directory exists and is writable
|
|
1531
|
+
- ⚠️ directory exists but is not writable
|
|
1532
|
+
- ❌ directory does not exist
|
|
1533
|
+
|
|
1534
|
+
Parameters:
|
|
1535
|
+
args (argparse.Namespace): Parsed CLI arguments used to determine the config search order.
|
|
1536
|
+
"""
|
|
1537
|
+
print("1. Testing configuration paths...")
|
|
1538
|
+
from mmrelay.config import get_config_paths
|
|
1539
|
+
|
|
1540
|
+
paths = get_config_paths(args)
|
|
1541
|
+
print(f" Config search paths: {len(paths)} locations")
|
|
1542
|
+
for i, path in enumerate(paths, 1):
|
|
1543
|
+
dir_path = os.path.dirname(path)
|
|
1544
|
+
dir_exists = os.path.exists(dir_path)
|
|
1545
|
+
dir_writable = os.access(dir_path, os.W_OK) if dir_exists else False
|
|
1546
|
+
status = "✅" if dir_exists and dir_writable else "⚠️" if dir_exists else "❌"
|
|
1547
|
+
print(f" {i}. {path} {status}")
|
|
1548
|
+
print()
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def _diagnose_sample_config_accessibility():
|
|
1552
|
+
"""
|
|
1553
|
+
Check availability of the bundled sample configuration and print a short diagnostic.
|
|
1554
|
+
|
|
1555
|
+
Performs two non-destructive checks and prints human-readable results:
|
|
1556
|
+
1) Verifies whether the sample config file exists at the path returned by mmrelay.tools.get_sample_config_path().
|
|
1557
|
+
2) Attempts to read the embedded resource "sample_config.yaml" from the mmrelay.tools package via importlib.resources and reports success and the content length.
|
|
1558
|
+
|
|
1559
|
+
Returns:
|
|
1560
|
+
bool: True if a filesystem sample config exists at the resolved path; False otherwise.
|
|
1561
|
+
"""
|
|
1562
|
+
print("2. Testing sample config accessibility...")
|
|
1563
|
+
from mmrelay.tools import get_sample_config_path
|
|
1564
|
+
|
|
1565
|
+
sample_path = get_sample_config_path()
|
|
1566
|
+
sample_exists = os.path.exists(sample_path)
|
|
1567
|
+
print(f" Sample config path: {sample_path}")
|
|
1568
|
+
print(f" Sample config exists: {'✅' if sample_exists else '❌'}")
|
|
1569
|
+
|
|
1570
|
+
# Test importlib.resources fallback
|
|
1571
|
+
try:
|
|
1572
|
+
import importlib.resources
|
|
1573
|
+
|
|
1574
|
+
content = (
|
|
1575
|
+
importlib.resources.files("mmrelay.tools")
|
|
1576
|
+
.joinpath("sample_config.yaml")
|
|
1577
|
+
.read_text()
|
|
1578
|
+
)
|
|
1579
|
+
print(f" importlib.resources fallback: ✅ ({len(content)} chars)")
|
|
1580
|
+
except (FileNotFoundError, ImportError, OSError) as e:
|
|
1581
|
+
print(f" importlib.resources fallback: ❌ ({e})")
|
|
1582
|
+
print()
|
|
1583
|
+
|
|
1584
|
+
return sample_exists
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def _diagnose_platform_specific(args):
|
|
1588
|
+
"""
|
|
1589
|
+
Run platform-specific diagnostic checks.
|
|
1590
|
+
|
|
1591
|
+
On Windows, imports and runs Windows-specific requirement checks and a configuration-generation
|
|
1592
|
+
test from mmrelay.windows_utils, printing per-component results and any warnings. On non-Windows
|
|
1593
|
+
platforms this reports that platform-specific tests are not required.
|
|
1594
|
+
|
|
1595
|
+
Parameters:
|
|
1596
|
+
args (argparse.Namespace): Parsed CLI arguments; passed through to the Windows
|
|
1597
|
+
config-generation test when running on Windows.
|
|
1598
|
+
|
|
1599
|
+
Returns:
|
|
1600
|
+
bool: True if the platform is Windows (Windows checks were executed), False otherwise.
|
|
1601
|
+
"""
|
|
1602
|
+
print("3. Platform-specific diagnostics...")
|
|
1603
|
+
import sys
|
|
1604
|
+
|
|
1605
|
+
from mmrelay.constants.app import WINDOWS_PLATFORM
|
|
1606
|
+
|
|
1607
|
+
on_windows = sys.platform == WINDOWS_PLATFORM
|
|
1608
|
+
print(f" Platform: {sys.platform}")
|
|
1609
|
+
print(f" Windows: {'Yes' if on_windows else 'No'}")
|
|
1610
|
+
|
|
1611
|
+
if on_windows:
|
|
1612
|
+
try:
|
|
1613
|
+
from mmrelay.windows_utils import (
|
|
1614
|
+
check_windows_requirements,
|
|
1615
|
+
test_config_generation_windows,
|
|
1616
|
+
)
|
|
1617
|
+
|
|
1618
|
+
# Check Windows requirements
|
|
1619
|
+
warnings = check_windows_requirements()
|
|
1620
|
+
if warnings:
|
|
1621
|
+
print(" Windows warnings: ⚠️")
|
|
1622
|
+
for line in warnings.split("\n"):
|
|
1623
|
+
if line.strip():
|
|
1624
|
+
print(f" {line}")
|
|
1625
|
+
else:
|
|
1626
|
+
print(" Windows compatibility: ✅")
|
|
1627
|
+
|
|
1628
|
+
# Run Windows-specific tests
|
|
1629
|
+
print("\n Windows config generation test:")
|
|
1630
|
+
results = test_config_generation_windows(args)
|
|
1631
|
+
|
|
1632
|
+
for component, result in results.items():
|
|
1633
|
+
if component == "overall_status":
|
|
1634
|
+
continue
|
|
1635
|
+
if isinstance(result, dict):
|
|
1636
|
+
status_icon = (
|
|
1637
|
+
"✅"
|
|
1638
|
+
if result["status"] == "ok"
|
|
1639
|
+
else "❌" if result["status"] == "error" else "⚠️"
|
|
1640
|
+
)
|
|
1641
|
+
print(f" {component}: {status_icon}")
|
|
1642
|
+
|
|
1643
|
+
overall = results.get("overall_status", "unknown")
|
|
1644
|
+
print(
|
|
1645
|
+
f" Overall Windows status: {'✅' if overall == 'ok' else '⚠️' if overall == 'partial' else '❌'}"
|
|
1646
|
+
)
|
|
1647
|
+
|
|
1648
|
+
except ImportError:
|
|
1649
|
+
print(" Windows utilities: ❌ (not available)")
|
|
1650
|
+
else:
|
|
1651
|
+
print(" Platform-specific tests: ✅ (Unix-like system)")
|
|
1652
|
+
|
|
1653
|
+
print()
|
|
1654
|
+
return on_windows
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
def _get_minimal_config_template():
|
|
1658
|
+
"""
|
|
1659
|
+
Return a minimal MMRelay YAML configuration template used as a fallback when the packaged sample_config.yaml cannot be located.
|
|
1660
|
+
|
|
1661
|
+
The template contains a small, functional configuration (matrix connection hints, a serial meshtastic connection, one room entry, and basic logging) that users can edit to create a working config file.
|
|
1662
|
+
|
|
1663
|
+
Returns:
|
|
1664
|
+
str: A YAML-formatted minimal configuration template.
|
|
1665
|
+
"""
|
|
1666
|
+
return """# MMRelay Configuration File
|
|
1667
|
+
# This is a minimal template created when the full sample config was unavailable
|
|
1668
|
+
# For complete configuration options, visit:
|
|
1669
|
+
# https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki
|
|
1670
|
+
|
|
1671
|
+
matrix:
|
|
1672
|
+
homeserver: https://matrix.example.org
|
|
1673
|
+
# Use 'mmrelay auth login' to set up authentication
|
|
1674
|
+
# access_token: your_access_token_here
|
|
1675
|
+
# bot_user_id: '@your_bot:matrix.example.org'
|
|
1676
|
+
|
|
1677
|
+
meshtastic:
|
|
1678
|
+
connection_type: serial
|
|
1679
|
+
serial_port: /dev/ttyUSB0 # Windows: COM3, macOS: /dev/cu.usbserial-*
|
|
1680
|
+
# host: meshtastic.local # For network connection
|
|
1681
|
+
# ble_address: "your_device_address" # For BLE connection
|
|
1682
|
+
|
|
1683
|
+
matrix_rooms:
|
|
1684
|
+
- id: '#your-room:matrix.example.org'
|
|
1685
|
+
meshtastic_channel: 0
|
|
1686
|
+
|
|
1687
|
+
logging:
|
|
1688
|
+
level: info
|
|
1689
|
+
|
|
1690
|
+
# Uncomment and configure as needed:
|
|
1691
|
+
# database:
|
|
1692
|
+
# msg_map:
|
|
1693
|
+
# msgs_to_keep: 100
|
|
1694
|
+
|
|
1695
|
+
# plugins:
|
|
1696
|
+
# ping:
|
|
1697
|
+
# active: true
|
|
1698
|
+
# weather:
|
|
1699
|
+
# active: true
|
|
1700
|
+
# units: metric
|
|
1701
|
+
"""
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def _diagnose_minimal_config_template():
|
|
1705
|
+
"""
|
|
1706
|
+
Validate the built-in minimal YAML configuration template and print a concise pass/fail status.
|
|
1707
|
+
|
|
1708
|
+
Attempts to parse the string returned by _get_minimal_config_template() using yaml.safe_load. Prints a single-line result showing a ✅ with the template character length when the template is valid YAML, or a ❌ with the YAML parsing error when invalid. This is a non-destructive diagnostic helper that prints output and does not return a value.
|
|
1709
|
+
"""
|
|
1710
|
+
print("4. Testing minimal config template fallback...")
|
|
1711
|
+
try:
|
|
1712
|
+
template = _get_minimal_config_template()
|
|
1713
|
+
yaml.safe_load(template)
|
|
1714
|
+
print(f" Minimal template: ✅ ({len(template)} chars, valid YAML)")
|
|
1715
|
+
except yaml.YAMLError as e:
|
|
1716
|
+
print(f" Minimal template: ❌ ({e})")
|
|
1717
|
+
|
|
1718
|
+
print()
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
def handle_config_diagnose(args):
|
|
1722
|
+
"""
|
|
1723
|
+
Run non-destructive diagnostics for the MMRelay configuration subsystem and print a human-readable report.
|
|
1724
|
+
|
|
1725
|
+
Performs four checks: resolves candidate config paths and reports directory accessibility; verifies packaged sample config availability (filesystem and importlib.resources fallback); runs platform-specific diagnostics (Windows-focused where applicable); and validates the built-in minimal YAML template. Prints findings and suggested next steps to stdout/stderr.
|
|
1726
|
+
|
|
1727
|
+
Parameters:
|
|
1728
|
+
args (argparse.Namespace): Parsed CLI arguments used to resolve config search paths and to control platform-specific checks.
|
|
1729
|
+
|
|
1730
|
+
Returns:
|
|
1731
|
+
int: Exit code (0 on success, 1 on failure).
|
|
1732
|
+
"""
|
|
1733
|
+
print("MMRelay Configuration System Diagnostics")
|
|
1734
|
+
print("=" * 40)
|
|
1735
|
+
print()
|
|
1736
|
+
|
|
1737
|
+
try:
|
|
1738
|
+
# Test 1: Basic config path resolution
|
|
1739
|
+
_diagnose_config_paths(args)
|
|
1740
|
+
|
|
1741
|
+
# Test 2: Sample config accessibility
|
|
1742
|
+
sample_exists = _diagnose_sample_config_accessibility()
|
|
1743
|
+
|
|
1744
|
+
# Test 3: Platform-specific diagnostics
|
|
1745
|
+
on_windows = _diagnose_platform_specific(args)
|
|
1746
|
+
|
|
1747
|
+
# Test 4: Minimal config template
|
|
1748
|
+
_diagnose_minimal_config_template()
|
|
1749
|
+
|
|
1750
|
+
print("=" * 40)
|
|
1751
|
+
print("Diagnostics complete!")
|
|
1752
|
+
|
|
1753
|
+
# Provide guidance based on results
|
|
1754
|
+
if on_windows and not sample_exists:
|
|
1755
|
+
print("\n💡 Windows Troubleshooting Tips:")
|
|
1756
|
+
print(" • Try: pip install --upgrade --force-reinstall mmrelay")
|
|
1757
|
+
print(" • Use: python -m mmrelay config generate")
|
|
1758
|
+
print(" • Check antivirus software for quarantined files")
|
|
1759
|
+
|
|
1760
|
+
return 0
|
|
1761
|
+
|
|
1762
|
+
except Exception as e:
|
|
1763
|
+
print(f"❌ Diagnostics failed: {e}", file=sys.stderr)
|
|
1764
|
+
|
|
1765
|
+
# Provide platform-specific guidance
|
|
1766
|
+
try:
|
|
1767
|
+
from mmrelay.windows_utils import get_windows_error_message, is_windows
|
|
1768
|
+
|
|
1769
|
+
if is_windows():
|
|
1770
|
+
error_msg = get_windows_error_message(e)
|
|
1771
|
+
print(f"\nWindows-specific guidance: {error_msg}", file=sys.stderr)
|
|
1772
|
+
except ImportError:
|
|
1773
|
+
pass
|
|
1774
|
+
|
|
1775
|
+
return 1
|
|
1776
|
+
|
|
1777
|
+
|
|
1450
1778
|
if __name__ == "__main__":
|
|
1451
1779
|
import sys
|
|
1452
1780
|
|
|
@@ -1509,7 +1837,8 @@ def generate_sample_config():
|
|
|
1509
1837
|
If an existing config file is found in any candidate path (from get_config_paths()), this function aborts and prints its location. Otherwise it creates a sample config at the first candidate path. Sources tried, in order, are:
|
|
1510
1838
|
- the path returned by get_sample_config_path(),
|
|
1511
1839
|
- the packaged resource mmrelay.tools:sample_config.yaml via importlib.resources,
|
|
1512
|
-
- a set of fallback filesystem locations relative to the package and current working directory
|
|
1840
|
+
- a set of fallback filesystem locations relative to the package and current working directory,
|
|
1841
|
+
- a minimal configuration template as a last resort.
|
|
1513
1842
|
|
|
1514
1843
|
On success the sample is written to disk and (on Unix-like systems) secure file permissions are applied (owner read/write, 0o600). Returns True when a sample config is successfully generated and False on any error or if a config already exists.
|
|
1515
1844
|
"""
|
|
@@ -1554,7 +1883,17 @@ def generate_sample_config():
|
|
|
1554
1883
|
)
|
|
1555
1884
|
return True
|
|
1556
1885
|
except (IOError, OSError) as e:
|
|
1557
|
-
|
|
1886
|
+
# Provide Windows-specific error guidance if available
|
|
1887
|
+
try:
|
|
1888
|
+
from mmrelay.windows_utils import get_windows_error_message, is_windows
|
|
1889
|
+
|
|
1890
|
+
if is_windows():
|
|
1891
|
+
error_msg = get_windows_error_message(e)
|
|
1892
|
+
print(f"Error copying sample config file: {error_msg}")
|
|
1893
|
+
else:
|
|
1894
|
+
print(f"Error copying sample config file: {e}")
|
|
1895
|
+
except ImportError:
|
|
1896
|
+
print(f"Error copying sample config file: {e}")
|
|
1558
1897
|
return False
|
|
1559
1898
|
|
|
1560
1899
|
# If the helper function failed, try using importlib.resources directly
|
|
@@ -1567,7 +1906,7 @@ def generate_sample_config():
|
|
|
1567
1906
|
)
|
|
1568
1907
|
|
|
1569
1908
|
# Write the sample config to the target path
|
|
1570
|
-
with open(target_path, "w") as f:
|
|
1909
|
+
with open(target_path, "w", encoding="utf-8") as f:
|
|
1571
1910
|
f.write(sample_config_content)
|
|
1572
1911
|
|
|
1573
1912
|
# Set secure permissions on Unix systems (600 - owner read/write)
|
|
@@ -1579,7 +1918,17 @@ def generate_sample_config():
|
|
|
1579
1918
|
)
|
|
1580
1919
|
return True
|
|
1581
1920
|
except (FileNotFoundError, ImportError, OSError) as e:
|
|
1582
|
-
print(f"Error accessing sample_config.yaml: {e}")
|
|
1921
|
+
print(f"Error accessing sample_config.yaml via importlib.resources: {e}")
|
|
1922
|
+
|
|
1923
|
+
# Provide Windows-specific guidance if needed
|
|
1924
|
+
try:
|
|
1925
|
+
from mmrelay.windows_utils import is_windows
|
|
1926
|
+
|
|
1927
|
+
if is_windows():
|
|
1928
|
+
print("This may be due to Windows installer packaging differences.")
|
|
1929
|
+
print("Trying alternative methods...")
|
|
1930
|
+
except ImportError:
|
|
1931
|
+
pass
|
|
1583
1932
|
|
|
1584
1933
|
# Fallback to traditional file paths if importlib.resources fails
|
|
1585
1934
|
# First, check in the package directory
|
|
@@ -1607,8 +1956,61 @@ def generate_sample_config():
|
|
|
1607
1956
|
)
|
|
1608
1957
|
return True
|
|
1609
1958
|
except (IOError, OSError) as e:
|
|
1610
|
-
|
|
1959
|
+
# Provide Windows-specific error guidance if available
|
|
1960
|
+
try:
|
|
1961
|
+
from mmrelay.windows_utils import (
|
|
1962
|
+
get_windows_error_message,
|
|
1963
|
+
is_windows,
|
|
1964
|
+
)
|
|
1965
|
+
|
|
1966
|
+
if is_windows():
|
|
1967
|
+
error_msg = get_windows_error_message(e)
|
|
1968
|
+
print(
|
|
1969
|
+
f"Error copying sample config file from {path}: {error_msg}"
|
|
1970
|
+
)
|
|
1971
|
+
else:
|
|
1972
|
+
print(f"Error copying sample config file from {path}: {e}")
|
|
1973
|
+
except ImportError:
|
|
1974
|
+
print(f"Error copying sample config file from {path}: {e}")
|
|
1611
1975
|
return False
|
|
1612
1976
|
|
|
1613
|
-
print("Error: Could not find sample_config.yaml")
|
|
1977
|
+
print("Error: Could not find sample_config.yaml in any location")
|
|
1978
|
+
|
|
1979
|
+
# Last resort: create a minimal config template
|
|
1980
|
+
print("\nAttempting to create minimal config template...")
|
|
1981
|
+
try:
|
|
1982
|
+
minimal_config = _get_minimal_config_template()
|
|
1983
|
+
with open(target_path, "w", encoding="utf-8") as f:
|
|
1984
|
+
f.write(minimal_config)
|
|
1985
|
+
|
|
1986
|
+
# Set secure permissions on Unix systems
|
|
1987
|
+
set_secure_file_permissions(target_path)
|
|
1988
|
+
|
|
1989
|
+
print(f"Created minimal config template at: {target_path}")
|
|
1990
|
+
print(
|
|
1991
|
+
"\n⚠️ This is a minimal template. Please refer to documentation for full configuration options."
|
|
1992
|
+
)
|
|
1993
|
+
print("Visit: https://github.com/jeremiah-k/meshtastic-matrix-relay/wiki")
|
|
1994
|
+
return True
|
|
1995
|
+
|
|
1996
|
+
except (IOError, OSError) as e:
|
|
1997
|
+
print(f"Failed to create minimal config template: {e}")
|
|
1998
|
+
|
|
1999
|
+
# Provide Windows-specific troubleshooting guidance
|
|
2000
|
+
try:
|
|
2001
|
+
from mmrelay.windows_utils import is_windows
|
|
2002
|
+
|
|
2003
|
+
if is_windows():
|
|
2004
|
+
print("\nWindows Troubleshooting:")
|
|
2005
|
+
print("1. Check if MMRelay was installed correctly")
|
|
2006
|
+
print("2. Try reinstalling with: pipx install --force mmrelay")
|
|
2007
|
+
print(
|
|
2008
|
+
"3. Use alternative entry point: python -m mmrelay config generate"
|
|
2009
|
+
)
|
|
2010
|
+
print("4. Check antivirus software - it may have quarantined files")
|
|
2011
|
+
print("5. Run diagnostics: python -m mmrelay config diagnose")
|
|
2012
|
+
print("6. Manually create config file using documentation")
|
|
2013
|
+
except ImportError:
|
|
2014
|
+
pass
|
|
2015
|
+
|
|
1614
2016
|
return False
|