mmrelay 1.2.6__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.
- mmrelay/__init__.py +5 -0
- mmrelay/__main__.py +29 -0
- mmrelay/cli.py +2013 -0
- mmrelay/cli_utils.py +746 -0
- mmrelay/config.py +956 -0
- mmrelay/constants/__init__.py +65 -0
- mmrelay/constants/app.py +29 -0
- mmrelay/constants/config.py +78 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +45 -0
- mmrelay/constants/network.py +45 -0
- mmrelay/constants/plugins.py +42 -0
- mmrelay/constants/queue.py +20 -0
- mmrelay/db_runtime.py +269 -0
- mmrelay/db_utils.py +1017 -0
- mmrelay/e2ee_utils.py +400 -0
- mmrelay/log_utils.py +274 -0
- mmrelay/main.py +439 -0
- mmrelay/matrix_utils.py +3091 -0
- mmrelay/meshtastic_utils.py +1245 -0
- mmrelay/message_queue.py +647 -0
- mmrelay/plugin_loader.py +1933 -0
- mmrelay/plugins/__init__.py +3 -0
- mmrelay/plugins/base_plugin.py +638 -0
- mmrelay/plugins/debug_plugin.py +30 -0
- mmrelay/plugins/drop_plugin.py +127 -0
- mmrelay/plugins/health_plugin.py +64 -0
- mmrelay/plugins/help_plugin.py +79 -0
- mmrelay/plugins/map_plugin.py +353 -0
- mmrelay/plugins/mesh_relay_plugin.py +222 -0
- mmrelay/plugins/nodes_plugin.py +92 -0
- mmrelay/plugins/ping_plugin.py +128 -0
- mmrelay/plugins/telemetry_plugin.py +179 -0
- mmrelay/plugins/weather_plugin.py +312 -0
- mmrelay/runtime_utils.py +35 -0
- mmrelay/setup_utils.py +828 -0
- mmrelay/tools/__init__.py +27 -0
- mmrelay/tools/mmrelay.service +19 -0
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +30 -0
- mmrelay/tools/sample-docker-compose.yaml +30 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +120 -0
- mmrelay/windows_utils.py +346 -0
- mmrelay-1.2.6.dist-info/METADATA +145 -0
- mmrelay-1.2.6.dist-info/RECORD +50 -0
- mmrelay-1.2.6.dist-info/WHEEL +5 -0
- mmrelay-1.2.6.dist-info/entry_points.txt +2 -0
- mmrelay-1.2.6.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tools and resources for MMRelay."""
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_sample_config_path():
|
|
7
|
+
"""
|
|
8
|
+
Provide the filesystem path to the package's sample configuration file.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
path (str): Path to `sample_config.yaml` located in the `mmrelay.tools` package.
|
|
12
|
+
"""
|
|
13
|
+
return str(
|
|
14
|
+
importlib.resources.files("mmrelay.tools").joinpath("sample_config.yaml")
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_service_template_path():
|
|
19
|
+
"""
|
|
20
|
+
Return the filesystem path to the mmrelay.service template bundled with the package.
|
|
21
|
+
|
|
22
|
+
Locate the `mmrelay.service` resource inside the `mmrelay.tools` package and return its filesystem path as a string.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
path (str): Filesystem path to the `mmrelay.service` template within the `mmrelay.tools` package.
|
|
26
|
+
"""
|
|
27
|
+
return str(importlib.resources.files("mmrelay.tools").joinpath("mmrelay.service"))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=MMRelay - Meshtastic <=> Matrix Relay
|
|
3
|
+
After=network-online.target
|
|
4
|
+
Wants=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=simple
|
|
8
|
+
# The mmrelay binary can be installed via pipx or pip
|
|
9
|
+
ExecStart=%h/.local/bin/mmrelay --config %h/.mmrelay/config.yaml --logfile %h/.mmrelay/logs/mmrelay.log
|
|
10
|
+
WorkingDirectory=%h/.mmrelay
|
|
11
|
+
Restart=on-failure
|
|
12
|
+
RestartSec=10
|
|
13
|
+
Environment=PYTHONUNBUFFERED=1
|
|
14
|
+
Environment=LANG=C.UTF-8
|
|
15
|
+
# Ensure both pipx and pip environments are properly loaded
|
|
16
|
+
Environment=PATH=%h/.local/bin:%h/.local/pipx/venvs/mmrelay/bin:/usr/local/bin:/usr/bin:/bin
|
|
17
|
+
|
|
18
|
+
[Install]
|
|
19
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
services:
|
|
2
|
+
mmrelay:
|
|
3
|
+
image: ghcr.io/jeremiah-k/mmrelay:latest
|
|
4
|
+
container_name: meshtastic-matrix-relay
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
stop_grace_period: 30s
|
|
7
|
+
user: "${UID:-1000}:${GID:-1000}"
|
|
8
|
+
environment:
|
|
9
|
+
- TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
|
|
10
|
+
|
|
11
|
+
volumes:
|
|
12
|
+
# Mount your config directory - create ~/.mmrelay/config.yaml first
|
|
13
|
+
# See docs/DOCKER.md for setup instructions
|
|
14
|
+
# For non-SELinux systems (most common):
|
|
15
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
16
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
17
|
+
# For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
|
|
18
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
|
|
19
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
|
|
20
|
+
|
|
21
|
+
# For BLE connections, uncomment these lines (Linux only):
|
|
22
|
+
# - /var/run/dbus:/var/run/dbus:ro
|
|
23
|
+
|
|
24
|
+
# For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
|
|
25
|
+
# network_mode: host
|
|
26
|
+
# security_opt:
|
|
27
|
+
# - apparmor=unconfined # Recommended for BLE to allow DBus communication
|
|
28
|
+
# privileged: true # Alternative if apparmor=unconfined is not acceptable
|
|
29
|
+
|
|
30
|
+
# Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
services:
|
|
2
|
+
mmrelay:
|
|
3
|
+
build: .
|
|
4
|
+
container_name: mmrelay # Updated for consistent branding
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
stop_grace_period: 30s
|
|
7
|
+
user: "${UID:-1000}:${GID:-1000}"
|
|
8
|
+
environment:
|
|
9
|
+
- TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
|
|
10
|
+
|
|
11
|
+
volumes:
|
|
12
|
+
# Mount your config directory - create ~/.mmrelay/config.yaml first
|
|
13
|
+
# Run 'make config' to set up the files
|
|
14
|
+
# For non-SELinux systems (most common):
|
|
15
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
16
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
17
|
+
# For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
|
|
18
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
|
|
19
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
|
|
20
|
+
|
|
21
|
+
# For BLE connections, uncomment these lines (Linux only):
|
|
22
|
+
# - /var/run/dbus:/var/run/dbus:ro
|
|
23
|
+
|
|
24
|
+
# For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
|
|
25
|
+
# network_mode: host
|
|
26
|
+
# security_opt:
|
|
27
|
+
# - apparmor=unconfined # Recommended for BLE to allow DBus communication
|
|
28
|
+
# privileged: true # Alternative if apparmor=unconfined is not acceptable
|
|
29
|
+
|
|
30
|
+
# Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
|
mmrelay/tools/sample.env
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Docker Compose environment variables
|
|
2
|
+
# Customize these paths if needed
|
|
3
|
+
|
|
4
|
+
# Base directory for mmrelay data
|
|
5
|
+
# This will be expanded by your shell when docker compose runs
|
|
6
|
+
MMRELAY_HOME=$HOME
|
|
7
|
+
|
|
8
|
+
# Preferred editor for config editing
|
|
9
|
+
# Will be set automatically when you select an editor via 'make edit'
|
|
10
|
+
EDITOR=nano
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
matrix:
|
|
2
|
+
homeserver: https://example.matrix.org
|
|
3
|
+
# Modern authentication using password (recommended)
|
|
4
|
+
# MMRelay will automatically create secure credentials.json on startup
|
|
5
|
+
password: your_matrix_password_here # Set your Matrix account password
|
|
6
|
+
# Security: After first successful start, remove this password from the file.
|
|
7
|
+
# Linux/macOS: chmod 600 ~/.mmrelay/config.yaml
|
|
8
|
+
# Windows: restrict file access to your user (e.g., via file Properties → Security)
|
|
9
|
+
# Never commit this file with a password to version control.
|
|
10
|
+
bot_user_id: "@botuser:example.matrix.org"
|
|
11
|
+
|
|
12
|
+
# End-to-End Encryption (E2EE) configuration
|
|
13
|
+
# E2EE is automatically enabled when using password-based authentication AND E2EE dependencies are installed (Linux/macOS only)
|
|
14
|
+
# NOTE: E2EE is not available on Windows due to dependency limitations
|
|
15
|
+
#
|
|
16
|
+
# SETUP INSTRUCTIONS:
|
|
17
|
+
# 1. Install E2EE dependencies: pipx install 'mmrelay[e2e]' (Linux/macOS only)
|
|
18
|
+
# 2. Set your password above and run MMRelay
|
|
19
|
+
# 3. MMRelay will automatically create credentials.json with E2EE support
|
|
20
|
+
# 4. For interactive setup, use: mmrelay auth login
|
|
21
|
+
#
|
|
22
|
+
#e2ee:
|
|
23
|
+
# # Optional: When credentials.json is present, MMRelay auto-enables E2EE.
|
|
24
|
+
# # Configure this section only if you want to override defaults (e.g., store_path).
|
|
25
|
+
# enabled: false # Explicit toggle if you need to force-enable/disable (usually not needed)
|
|
26
|
+
# store_path: ~/.mmrelay/store # Optional path for encryption keys storage
|
|
27
|
+
|
|
28
|
+
# Message prefix customization (Meshtastic → Matrix direction)
|
|
29
|
+
#prefix_enabled: true # Enable prefixes on messages from mesh (e.g., "[Alice/MyMesh]: message")
|
|
30
|
+
#prefix_format: "[{long}/{mesh}]: " # Default format. Variables: {long1-20}, {long}, {short}, {mesh1-20}, {mesh}
|
|
31
|
+
|
|
32
|
+
matrix_rooms: # Needs at least 1 room & channel, but supports all Meshtastic channels
|
|
33
|
+
- id: "#someroomalias:example.matrix.org" # Matrix room aliases & IDs supported
|
|
34
|
+
meshtastic_channel: 0
|
|
35
|
+
- id: "!someroomid:example.matrix.org"
|
|
36
|
+
meshtastic_channel: 2
|
|
37
|
+
|
|
38
|
+
meshtastic:
|
|
39
|
+
connection_type: tcp # Choose either "tcp", "serial", or "ble"
|
|
40
|
+
host: meshtastic.local # Only used when connection is "tcp"
|
|
41
|
+
serial_port: /dev/ttyUSB0 # Only used when connection is "serial"
|
|
42
|
+
ble_address: AA:BB:CC:DD:EE:FF # Only used when connection is "ble" - Uses either an address or name from a `meshtastic --ble-scan`
|
|
43
|
+
meshnet_name: Your Meshnet Name # This is displayed in full on Matrix, but is truncated when sent to a Meshnet
|
|
44
|
+
message_interactions: # Configure reactions and replies (both require message storage in database)
|
|
45
|
+
reactions: false # Enable reaction relaying between platforms
|
|
46
|
+
replies: false # Enable reply relaying between platforms
|
|
47
|
+
|
|
48
|
+
# Connection health monitoring configuration
|
|
49
|
+
#health_check:
|
|
50
|
+
# enabled: true # Enable/disable periodic health checks (default: true)
|
|
51
|
+
# heartbeat_interval: 60 # Interval in seconds between health checks (default: 60)
|
|
52
|
+
# # Note: BLE connections use real-time disconnection detection and skip periodic checks
|
|
53
|
+
# # Legacy: heartbeat_interval at meshtastic level still supported but deprecated
|
|
54
|
+
|
|
55
|
+
# Additional configuration options (commented out with defaults)
|
|
56
|
+
broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
|
|
57
|
+
#detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
|
|
58
|
+
#message_delay: 2.2 # Delay in seconds between messages sent to mesh (minimum: 2.0 due to firmware)
|
|
59
|
+
|
|
60
|
+
# Message prefix customization (Matrix → Meshtastic direction)
|
|
61
|
+
#prefix_enabled: true # Enable username prefixes on messages sent to mesh (e.g., "Alice[M]: message")
|
|
62
|
+
#prefix_format: "{display5}[M]: " # Default format. See ADVANCED_CONFIGURATION.md for all variables.
|
|
63
|
+
|
|
64
|
+
logging:
|
|
65
|
+
level: info
|
|
66
|
+
#log_to_file: true # Set to true to enable file logging
|
|
67
|
+
#filename: ~/.mmrelay/logs/mmrelay.log # Default location if log_to_file is true
|
|
68
|
+
#max_log_size: 10485760 # 10 MB default if omitted
|
|
69
|
+
#backup_count: 1 # Keeps 1 backup as the default if omitted
|
|
70
|
+
#color_enabled: true # Set to false to disable colored console output
|
|
71
|
+
|
|
72
|
+
# Component-specific debug logging (useful for troubleshooting)
|
|
73
|
+
# When disabled (false or omitted), external library logs are completely suppressed
|
|
74
|
+
# When enabled, you can use: true (DEBUG level) or specify level: "debug", "info", "warning", "error"
|
|
75
|
+
#debug:
|
|
76
|
+
# matrix_nio: false # Disable matrix-nio logging (default: completely suppressed)
|
|
77
|
+
# #matrix_nio: true # Enable matrix-nio debug logging
|
|
78
|
+
# #matrix_nio: "warning" # Enable matrix-nio warning+ logging
|
|
79
|
+
# bleak: false # Disable BLE (bleak) logging (default: completely suppressed)
|
|
80
|
+
# meshtastic: false # Disable meshtastic library logging (default: completely suppressed)
|
|
81
|
+
|
|
82
|
+
#database:
|
|
83
|
+
# path: ~/.mmrelay/data/meshtastic.sqlite # Default location
|
|
84
|
+
# enable_wal: true # Enable Write-Ahead Logging for better concurrency
|
|
85
|
+
# busy_timeout_ms: 5000 # Milliseconds to wait if the database is busy
|
|
86
|
+
# pragmas:
|
|
87
|
+
# synchronous: NORMAL
|
|
88
|
+
# temp_store: MEMORY
|
|
89
|
+
# msg_map: # The message map is necessary for the relay_reactions functionality. If `relay_reactions` is set to false, nothing will be saved to the message map.
|
|
90
|
+
# msgs_to_keep: 500 # If set to 0, it will not delete any messages; Defaults to 500
|
|
91
|
+
# wipe_on_restart: true # Clears out the message map when the relay is restarted; Defaults to False
|
|
92
|
+
|
|
93
|
+
# These are core Plugins - Note: Some plugins are experimental and some need maintenance.
|
|
94
|
+
plugins:
|
|
95
|
+
ping:
|
|
96
|
+
active: true
|
|
97
|
+
#channels: [2,3,5] # List of channels the plugin will respond to; DMs are always processed if the plugin is active
|
|
98
|
+
weather:
|
|
99
|
+
active: true
|
|
100
|
+
units: imperial # Options: metric, imperial - Default is metric
|
|
101
|
+
#channels: [] # Empty list, will only respond to DMs
|
|
102
|
+
nodes:
|
|
103
|
+
active: true
|
|
104
|
+
# Does not need to specify channels, as it's a Matrix-only plugin
|
|
105
|
+
|
|
106
|
+
#community-plugins:
|
|
107
|
+
# sample_plugin:
|
|
108
|
+
# active: true
|
|
109
|
+
# repository: https://github.com/username/sample_plugin.git
|
|
110
|
+
# tag: master
|
|
111
|
+
# advanced_plugin:
|
|
112
|
+
# active: false
|
|
113
|
+
# repository: https://github.com/username/advanced_plugin.git
|
|
114
|
+
# tag: v1.2.0
|
|
115
|
+
|
|
116
|
+
#custom-plugins:
|
|
117
|
+
# my_custom_plugin:
|
|
118
|
+
# active: true
|
|
119
|
+
# another_custom_plugin:
|
|
120
|
+
# active: false
|
mmrelay/windows_utils.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows-specific utilities for MMRelay.
|
|
3
|
+
|
|
4
|
+
This module provides Windows-specific functionality and workarounds
|
|
5
|
+
for better compatibility and user experience on Windows systems.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from mmrelay.constants.app import WINDOWS_PLATFORM
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_windows() -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Return True if the current process is running on a Windows platform.
|
|
18
|
+
|
|
19
|
+
Checks common platform indicators (os.name == "nt" or sys.platform == WINDOWS_PLATFORM)
|
|
20
|
+
and returns a boolean accordingly.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
bool: True when running on Windows, otherwise False.
|
|
24
|
+
"""
|
|
25
|
+
return os.name == "nt" or sys.platform == WINDOWS_PLATFORM
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_windows_console() -> None:
|
|
29
|
+
"""
|
|
30
|
+
Configure the Windows console for UTF-8 output and ANSI (VT100) color support.
|
|
31
|
+
|
|
32
|
+
Best-effort operation: on Windows this attempts to set stdout/stderr encoding to UTF-8
|
|
33
|
+
(if supported) and enable Virtual Terminal Processing so ANSI color sequences and
|
|
34
|
+
Unicode render correctly. No-op on non-Windows platforms; failures are silently ignored.
|
|
35
|
+
"""
|
|
36
|
+
if not is_windows():
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Enable UTF-8 output on Windows
|
|
41
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
42
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
43
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
44
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
# Enable ANSI color codes on Windows 10+
|
|
47
|
+
import ctypes
|
|
48
|
+
|
|
49
|
+
kernel32 = ctypes.windll.kernel32
|
|
50
|
+
ENABLE_VTP = 0x0004 # ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
51
|
+
for handle in (-11, -12): # STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
|
|
52
|
+
h = kernel32.GetStdHandle(handle)
|
|
53
|
+
if h is not None and h != -1:
|
|
54
|
+
mode = ctypes.c_uint()
|
|
55
|
+
if kernel32.GetConsoleMode(h, ctypes.byref(mode)):
|
|
56
|
+
kernel32.SetConsoleMode(h, mode.value | ENABLE_VTP)
|
|
57
|
+
except (OSError, AttributeError):
|
|
58
|
+
# If console setup fails, continue without it
|
|
59
|
+
# This is expected on non-Windows systems or older Windows versions
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_windows_error_message(error: Exception) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Return a Windows-tailored, user-friendly message for common filesystem and network exceptions.
|
|
66
|
+
|
|
67
|
+
If not running on Windows this returns str(error) unchanged. For Windows, the function recognizes
|
|
68
|
+
permission errors (PermissionError or OSError with EACCES/EPERM), missing-file errors
|
|
69
|
+
(FileNotFoundError or OSError with ENOENT), and common network-related OSErrors
|
|
70
|
+
(EHOSTUNREACH, ENETDOWN, ENETUNREACH, ECONNREFUSED, ETIMEDOUT) and returns guidance specific
|
|
71
|
+
to each case (possible causes and suggested next steps). For other exceptions it returns
|
|
72
|
+
str(error).
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
error (Exception): The exception to translate into a user-facing message.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
str: A user-oriented error message; either a Windows-specific guidance string or the
|
|
79
|
+
original exception string for unhandled cases or non-Windows platforms.
|
|
80
|
+
"""
|
|
81
|
+
if not is_windows():
|
|
82
|
+
return str(error)
|
|
83
|
+
|
|
84
|
+
import errno as _errno
|
|
85
|
+
|
|
86
|
+
# Use exception types and errno codes for more robust error detection
|
|
87
|
+
if isinstance(error, PermissionError) or (
|
|
88
|
+
isinstance(error, OSError) and error.errno in {_errno.EACCES, _errno.EPERM}
|
|
89
|
+
):
|
|
90
|
+
return (
|
|
91
|
+
f"Permission denied: {error}\n"
|
|
92
|
+
"This may be caused by:\n"
|
|
93
|
+
"• Antivirus software blocking the operation\n"
|
|
94
|
+
"• Windows User Account Control (UAC) restrictions\n"
|
|
95
|
+
"• File being used by another process\n"
|
|
96
|
+
"Try running as administrator or check antivirus settings."
|
|
97
|
+
)
|
|
98
|
+
elif isinstance(error, FileNotFoundError) or (
|
|
99
|
+
isinstance(error, OSError) and error.errno in {_errno.ENOENT}
|
|
100
|
+
):
|
|
101
|
+
return (
|
|
102
|
+
f"File not found: {error}\n"
|
|
103
|
+
"This may be caused by:\n"
|
|
104
|
+
"• Incorrect file path (check for spaces or special characters)\n"
|
|
105
|
+
"• File moved or deleted by antivirus software\n"
|
|
106
|
+
"• Network drive disconnection\n"
|
|
107
|
+
"Verify the file path and check antivirus quarantine."
|
|
108
|
+
)
|
|
109
|
+
elif isinstance(error, ConnectionError) or (
|
|
110
|
+
isinstance(error, OSError)
|
|
111
|
+
and error.errno
|
|
112
|
+
in {
|
|
113
|
+
_errno.EHOSTUNREACH,
|
|
114
|
+
_errno.ENETDOWN,
|
|
115
|
+
_errno.ENETUNREACH,
|
|
116
|
+
_errno.ECONNREFUSED,
|
|
117
|
+
_errno.ETIMEDOUT,
|
|
118
|
+
}
|
|
119
|
+
):
|
|
120
|
+
return (
|
|
121
|
+
f"Network error: {error}\n"
|
|
122
|
+
"This may be caused by:\n"
|
|
123
|
+
"• Windows Firewall blocking the connection\n"
|
|
124
|
+
"• Antivirus software blocking network access\n"
|
|
125
|
+
"• VPN or proxy configuration issues\n"
|
|
126
|
+
"Check firewall settings and antivirus network protection."
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
return str(error)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_windows_requirements() -> Optional[str]:
|
|
133
|
+
"""
|
|
134
|
+
Report Windows environment compatibility issues as a multi-line warning string.
|
|
135
|
+
|
|
136
|
+
When running on Windows, performs these checks and accumulates user-facing warnings:
|
|
137
|
+
- Python version is older than 3.10.
|
|
138
|
+
- Process does not appear to be running inside a virtual environment (venv/pipx).
|
|
139
|
+
- Current working directory path length exceeds 200 characters.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Optional[str]: Multi-line warning message prefixed with "Windows compatibility warnings:" and bullet points for each detected issue; `None` if no issues are detected or when not running on Windows.
|
|
143
|
+
"""
|
|
144
|
+
if not is_windows():
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
warnings = []
|
|
148
|
+
|
|
149
|
+
# Check Python version for Windows compatibility
|
|
150
|
+
if sys.version_info < (3, 10):
|
|
151
|
+
warnings.append("Python 3.10+ is required for this application")
|
|
152
|
+
|
|
153
|
+
# Check if running in a virtual environment
|
|
154
|
+
if not hasattr(sys, "real_prefix") and not (
|
|
155
|
+
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
|
|
156
|
+
):
|
|
157
|
+
warnings.append(
|
|
158
|
+
"Consider using a virtual environment (venv) or pipx for better isolation"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Check for common Windows path issues
|
|
162
|
+
if len(os.getcwd()) > 200:
|
|
163
|
+
warnings.append(
|
|
164
|
+
"Current directory path is very long - this may cause issues on Windows"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if warnings:
|
|
168
|
+
return "Windows compatibility warnings:\n• " + "\n• ".join(warnings)
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_config_generation_windows(args=None) -> dict:
|
|
174
|
+
"""
|
|
175
|
+
Run Windows-only diagnostics for MMRelay configuration generation.
|
|
176
|
+
|
|
177
|
+
Performs four checks and returns a dictionary with per-test results and an overall status:
|
|
178
|
+
- sample_config_path: verifies mmrelay.tools.get_sample_config_path() exists.
|
|
179
|
+
- importlib_resources: attempts to read mmrelay.tools/sample_config.yaml via importlib.resources.
|
|
180
|
+
- config_paths: calls mmrelay.config.get_config_paths(args) and reports the returned paths.
|
|
181
|
+
- directory_creation: ensures parent directories for the config paths exist (creates them if missing).
|
|
182
|
+
|
|
183
|
+
Parameters:
|
|
184
|
+
args (optional): Forwarded to mmrelay.config.get_config_paths; typically CLI-style arguments or None.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
dict: Diagnostic results with these keys:
|
|
188
|
+
- sample_config_path, importlib_resources, config_paths, directory_creation:
|
|
189
|
+
dict objects with "status" ("ok" or "error") and "details" (string).
|
|
190
|
+
- overall_status: one of "ok" (no errors), "partial" (1–2 errors), or "error" (3+ errors).
|
|
191
|
+
If called on a non-Windows platform, returns {"error": "This function is only for Windows systems"}.
|
|
192
|
+
|
|
193
|
+
Notes:
|
|
194
|
+
- The function does not raise on expected failures; errors are reported in the returned dict.
|
|
195
|
+
- Directory creation test may create directories on disk when missing.
|
|
196
|
+
"""
|
|
197
|
+
if not is_windows():
|
|
198
|
+
return {"error": "This function is only for Windows systems"}
|
|
199
|
+
|
|
200
|
+
results = {
|
|
201
|
+
"sample_config_path": {"status": "unknown", "details": ""},
|
|
202
|
+
"importlib_resources": {"status": "unknown", "details": ""},
|
|
203
|
+
"config_paths": {"status": "unknown", "details": ""},
|
|
204
|
+
"directory_creation": {"status": "unknown", "details": ""},
|
|
205
|
+
"overall_status": "unknown",
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
# Test 1: Sample config path
|
|
210
|
+
try:
|
|
211
|
+
from mmrelay.tools import get_sample_config_path
|
|
212
|
+
|
|
213
|
+
sample_path = get_sample_config_path()
|
|
214
|
+
if os.path.exists(sample_path):
|
|
215
|
+
results["sample_config_path"] = {
|
|
216
|
+
"status": "ok",
|
|
217
|
+
"details": f"Found at: {sample_path}",
|
|
218
|
+
}
|
|
219
|
+
else:
|
|
220
|
+
results["sample_config_path"] = {
|
|
221
|
+
"status": "error",
|
|
222
|
+
"details": f"Not found at: {sample_path}",
|
|
223
|
+
}
|
|
224
|
+
except (
|
|
225
|
+
ImportError,
|
|
226
|
+
OSError,
|
|
227
|
+
FileNotFoundError,
|
|
228
|
+
AttributeError,
|
|
229
|
+
TypeError,
|
|
230
|
+
) as e:
|
|
231
|
+
results["sample_config_path"] = {"status": "error", "details": str(e)}
|
|
232
|
+
|
|
233
|
+
# Test 2: importlib.resources fallback
|
|
234
|
+
try:
|
|
235
|
+
import importlib.resources
|
|
236
|
+
|
|
237
|
+
content = (
|
|
238
|
+
importlib.resources.files("mmrelay.tools")
|
|
239
|
+
.joinpath("sample_config.yaml")
|
|
240
|
+
.read_text()
|
|
241
|
+
)
|
|
242
|
+
results["importlib_resources"] = {
|
|
243
|
+
"status": "ok",
|
|
244
|
+
"details": f"Content length: {len(content)} chars",
|
|
245
|
+
}
|
|
246
|
+
except (ImportError, OSError, FileNotFoundError) as e:
|
|
247
|
+
results["importlib_resources"] = {"status": "error", "details": str(e)}
|
|
248
|
+
|
|
249
|
+
# Test 3: Config paths
|
|
250
|
+
try:
|
|
251
|
+
from mmrelay.config import get_config_paths
|
|
252
|
+
|
|
253
|
+
paths = get_config_paths(args)
|
|
254
|
+
results["config_paths"] = {"status": "ok", "details": f"Paths: {paths}"}
|
|
255
|
+
except (ImportError, OSError) as e:
|
|
256
|
+
results["config_paths"] = {"status": "error", "details": str(e)}
|
|
257
|
+
|
|
258
|
+
# Test 4: Directory creation
|
|
259
|
+
try:
|
|
260
|
+
from mmrelay.config import get_config_paths
|
|
261
|
+
|
|
262
|
+
paths = get_config_paths(args)
|
|
263
|
+
created_dirs = []
|
|
264
|
+
for path in paths:
|
|
265
|
+
dir_path = os.path.dirname(path)
|
|
266
|
+
if not os.path.exists(dir_path):
|
|
267
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
268
|
+
created_dirs.append(dir_path)
|
|
269
|
+
results["directory_creation"] = {
|
|
270
|
+
"status": "ok",
|
|
271
|
+
"details": f"Created: {created_dirs}",
|
|
272
|
+
}
|
|
273
|
+
except OSError as e:
|
|
274
|
+
results["directory_creation"] = {"status": "error", "details": str(e)}
|
|
275
|
+
|
|
276
|
+
# Determine overall status
|
|
277
|
+
error_count = sum(
|
|
278
|
+
1
|
|
279
|
+
for r in results.values()
|
|
280
|
+
if isinstance(r, dict) and r.get("status") == "error"
|
|
281
|
+
)
|
|
282
|
+
if error_count == 0:
|
|
283
|
+
results["overall_status"] = "ok"
|
|
284
|
+
elif error_count < 3: # If at least one fallback works
|
|
285
|
+
results["overall_status"] = "partial"
|
|
286
|
+
else:
|
|
287
|
+
results["overall_status"] = "error"
|
|
288
|
+
|
|
289
|
+
except OSError as e:
|
|
290
|
+
results["overall_status"] = "error"
|
|
291
|
+
results["error"] = str(e)
|
|
292
|
+
|
|
293
|
+
return results
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_windows_install_guidance() -> str:
|
|
297
|
+
"""
|
|
298
|
+
Return a Windows-specific installation and troubleshooting guide as a formatted string.
|
|
299
|
+
|
|
300
|
+
The returned text contains recommended installation methods, common Windows-specific problems with actionable remedies, and troubleshooting tips for configuration and runtime issues. It is safe to display directly to end users or include in logs/help output.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
str: Multiline guidance text suited for Windows users.
|
|
304
|
+
"""
|
|
305
|
+
return """
|
|
306
|
+
Windows Installation & Troubleshooting Guide:
|
|
307
|
+
|
|
308
|
+
📦 Recommended Installation:
|
|
309
|
+
pipx install mmrelay
|
|
310
|
+
(pipx provides better isolation and fewer conflicts)
|
|
311
|
+
|
|
312
|
+
🔧 If pipx is not available:
|
|
313
|
+
pip install --user mmrelay
|
|
314
|
+
(installs to user directory, avoiding system conflicts)
|
|
315
|
+
|
|
316
|
+
⚠️ Common Windows Issues:
|
|
317
|
+
|
|
318
|
+
1. "ModuleNotFoundError: No module named 'pkg_resources'"
|
|
319
|
+
Solution: pip install --upgrade setuptools
|
|
320
|
+
Alternative: Use 'python -m mmrelay' instead of 'mmrelay'
|
|
321
|
+
|
|
322
|
+
2. "Access denied" or permission errors
|
|
323
|
+
Solution: Run command prompt as administrator
|
|
324
|
+
Or: Use --user flag with pip
|
|
325
|
+
|
|
326
|
+
3. "SSL certificate verify failed"
|
|
327
|
+
Solution: Update certificates or use --trusted-host flag
|
|
328
|
+
|
|
329
|
+
4. Antivirus blocking installation/execution
|
|
330
|
+
Solution: Add Python and pip to antivirus exclusions
|
|
331
|
+
|
|
332
|
+
5. Long path issues
|
|
333
|
+
Solution: Enable long path support in Windows 10+
|
|
334
|
+
Or: Use shorter installation directory
|
|
335
|
+
|
|
336
|
+
6. Config generation fails
|
|
337
|
+
Solution: Check if sample_config.yaml is accessible
|
|
338
|
+
Alternative: Manually create config file from documentation
|
|
339
|
+
|
|
340
|
+
🆘 Need Help?
|
|
341
|
+
• Check Windows Event Viewer for detailed error logs
|
|
342
|
+
• Temporarily disable antivirus for testing
|
|
343
|
+
• Use Windows PowerShell instead of Command Prompt
|
|
344
|
+
• Consider using Windows Subsystem for Linux (WSL)
|
|
345
|
+
• Test config generation: 'python -m mmrelay config diagnose'
|
|
346
|
+
"""
|