Add UDP broadcast functionality and Qt6 discovery application

- Added UDP broadcast component that broadcasts server info every 30 seconds
- Integrated UDP broadcast into main application core
- Created Qt6 discovery application for LAN detection
- Added auto-exit option when browser opens
- Created PyInstaller specs for Linux and Windows executables
- Added comprehensive testing suite
- Added setup script and documentation
- Updated requirements.txt with netifaces dependency

Features:
- Broadcasts MBetterClient server info with host/port/SSL status
- Cross-platform Qt6 discovery app with system tray support
- Automatic browser opening when servers are discovered
- Manual connection capability
- Real-time server list with timestamps
- Exit-after-open option for one-time usage
parent d419bb79
# MBetter Client Discovery Application
A Qt6-based LAN discovery tool that automatically detects MBetterClient servers on the local network and opens the dashboard in a web browser.
## Features
- **Automatic Discovery**: Listens for UDP broadcasts from MBetterClient servers on the local network
- **Cross-Platform**: Compatible with Linux and Windows
- **System Tray Support**: Runs minimized in the system tray
- **Auto-Open Browser**: Automatically opens discovered servers in the default web browser
- **Manual Connection**: Connect to servers manually by entering their URL
- **SSL Support Detection**: Identifies servers running with SSL/HTTPS enabled
- **Real-time Updates**: Shows discovered servers in real-time with timestamps
## Installation & Setup
### Quick Setup
1. Run the setup script to install dependencies:
```bash
python3 setup_discovery.py # Linux/Mac
python setup_discovery.py # Windows
```
2. Run the discovery application:
```bash
./run_discovery.sh # Linux/Mac
run_discovery.bat # Windows
```
### Manual Installation
1. Install required Python packages:
```bash
pip install PyQt6 netifaces
```
2. Run the application:
```bash
python3 mbetter_discovery.py
```
### Creating Standalone Executable
The setup script can create a standalone executable:
```bash
python3 setup_discovery.py
# When prompted, choose 'y' to create executable
```
The executable will be created in the `dist/` directory.
## How It Works
### UDP Broadcast Protocol
The MBetterClient main application broadcasts UDP packets every 30 seconds on port `45123` with the following JSON structure:
```json
{
"service": "MBetterClient",
"host": "192.168.1.100",
"port": 5001,
"ssl": false,
"url": "http://192.168.1.100:5001",
"timestamp": 1693910123.456
}
```
### Discovery Process
1. The discovery app listens on UDP port `45123` (configurable)
2. When a broadcast is received, it validates the JSON structure
3. If valid, the server is added to the discovered servers list
4. If auto-open is enabled, the browser opens the server's dashboard URL
5. The app shows a system tray notification about the discovery
## Usage
### Main Window
- **Settings Section**: Configure auto-open browser and listen port
- **Discovered Servers**: Shows all found servers with their URLs and discovery time
- **Manual Connection**: Enter a URL manually to connect to a server
- **Status & Log**: Real-time status and activity log
### System Tray
- Right-click the tray icon to access the menu
- Double-click to show/hide the main window
- The application continues running in the background when the window is closed
### Keyboard Shortcuts
- Double-click a server in the list to open it in the browser
- The application minimizes to tray when closed (if system tray is available)
## Configuration
### Settings
- **Auto-open Browser**: Automatically open discovered servers in the default browser
- **Listen Port**: UDP port to listen for broadcasts (default: 45123)
### Manual Connection
If you know the IP address of a MBetterClient server, you can connect manually:
1. Enter the URL in the "Manual URL" field (e.g., `http://192.168.1.100:5001`)
2. Click "Connect" to open it in the browser
## Network Requirements
### Firewall Configuration
Ensure the following ports are open:
- **UDP 45123**: For receiving broadcasts (incoming)
- **TCP 80/443**: For HTTP/HTTPS web access (outgoing)
### Network Discovery
The application works on:
- Local Area Networks (LAN)
- Wi-Fi networks
- Wired Ethernet networks
- VPN networks (if UDP broadcasts are allowed)
## Troubleshooting
### Common Issues
**No servers discovered:**
- Check that MBetterClient is running on the network
- Verify firewall settings allow UDP traffic on port 45123
- Ensure you're on the same network segment
- Try changing the listen port in settings
**Application won't start:**
- Ensure Python 3.7+ is installed
- Install required dependencies: `pip install PyQt6 netifaces`
- Check the log output for specific error messages
**Browser doesn't open automatically:**
- Check the "Auto-open browser" setting is enabled
- Manually click on discovered servers in the list
- Use the manual connection feature
**System tray not working:**
- Some Linux environments may not support system tray
- The application will still work without tray functionality
- Use the "Minimize to Tray" button to test tray support
### Debug Mode
Run with Python directly to see detailed logging:
```bash
python3 mbetter_discovery.py
```
Check the log output in the application window for troubleshooting information.
## Technical Details
### Dependencies
- **PyQt6**: GUI framework
- **netifaces**: Network interface detection (optional, improves broadcast detection)
- **Standard Python libraries**: socket, json, threading, webbrowser
### Protocol Details
- **Transport**: UDP broadcast
- **Port**: 45123 (configurable)
- **Format**: JSON
- **Frequency**: Every 30 seconds from MBetterClient servers
- **Scope**: Local network broadcast domain
### Security Notes
- UDP broadcasts are unencrypted and visible to all network users
- No authentication is required for discovery
- The discovery process only reads broadcast messages, it doesn't send any data
- Browser connections use the server's configured protocol (HTTP/HTTPS)
## Building from Source
### Development Setup
1. Clone or copy the discovery files:
- `mbetter_discovery.py` - Main application
- `setup_discovery.py` - Setup script
- `DISCOVERY_README.md` - This documentation
2. Install development dependencies:
```bash
pip install PyQt6 netifaces pyinstaller
```
3. Run directly:
```bash
python3 mbetter_discovery.py
```
### Creating Distribution
Use PyInstaller to create standalone executables:
```bash
# Single file executable
pyinstaller --onefile --windowed --name MBetterDiscovery mbetter_discovery.py
# Directory distribution (includes all dependencies)
pyinstaller --windowed --name MBetterDiscovery mbetter_discovery.py
```
## License
This discovery application is part of the MBetterClient project and follows the same licensing terms.
\ No newline at end of file
This diff is collapsed.
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller spec file for MBetter Discovery Application - Linux
"""
block_cipher = None
a = Analysis(
['mbetter_discovery.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[
'PyQt6.QtCore',
'PyQt6.QtGui',
'PyQt6.QtWidgets',
'netifaces',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter',
'matplotlib',
'numpy',
'PIL',
'cv2',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MBetterDiscovery',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # Set to True for debugging
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
\ No newline at end of file
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller spec file for MBetter Discovery Application - Windows
"""
block_cipher = None
a = Analysis(
['mbetter_discovery.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[
'PyQt6.QtCore',
'PyQt6.QtGui',
'PyQt6.QtWidgets',
'netifaces',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter',
'matplotlib',
'numpy',
'PIL',
'cv2',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MBetterDiscovery',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # Set to True for debugging
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=None, # Add path to .ico file if available
)
\ No newline at end of file
......@@ -15,6 +15,7 @@ from ..config.manager import ConfigManager
from ..database.manager import DatabaseManager
from .message_bus import MessageBus, Message, MessageType, MessageBuilder
from .thread_manager import ThreadManager
from .udp_broadcast import UDPBroadcastComponent
logger = logging.getLogger(__name__)
......@@ -44,6 +45,7 @@ class MbetterClientApplication:
self.screen_cast = None
self.games_thread = None
self.match_timer = None
self.udp_broadcast = None
# Main loop thread
self._main_loop_thread: Optional[threading.Thread] = None
......@@ -248,6 +250,13 @@ class MbetterClientApplication:
logger.error("Match timer initialization failed")
return False
# Initialize UDP broadcast component
if self._initialize_udp_broadcast():
components_initialized += 1
else:
logger.error("UDP broadcast initialization failed")
return False
if components_initialized == 0:
logger.error("No components were initialized")
return False
......@@ -438,6 +447,24 @@ class MbetterClientApplication:
logger.error(f"Match timer initialization failed: {e}")
return False
def _initialize_udp_broadcast(self) -> bool:
"""Initialize UDP broadcast component"""
try:
self.udp_broadcast = UDPBroadcastComponent(
message_bus=self.message_bus,
broadcast_port=45123 # Default UDP broadcast port
)
# Register with thread manager
self.thread_manager.register_component("udp_broadcast", self.udp_broadcast)
logger.info("UDP broadcast component initialized")
return True
except Exception as e:
logger.error(f"UDP broadcast initialization failed: {e}")
return False
def run(self) -> int:
"""Run the application"""
try:
......
"""
UDP broadcast component for LAN discovery
Broadcasts MBetterClient server information every 30 seconds
"""
import socket
import json
import time
import logging
import threading
from typing import Optional, Dict, Any
from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType
logger = logging.getLogger(__name__)
class UDPBroadcastComponent(ThreadedComponent):
"""Component for broadcasting server information via UDP"""
def __init__(self, message_bus: MessageBus, broadcast_port: int = 45123):
super().__init__("udp_broadcast", message_bus)
self.broadcast_port = broadcast_port
self.broadcast_interval = 30.0 # 30 seconds
self.socket: Optional[socket.socket] = None
# Server information to broadcast
self.server_info: Dict[str, Any] = {
"service": "MBetterClient",
"host": "127.0.0.1",
"port": 5001,
"ssl": False,
"url": "http://127.0.0.1:5001",
"timestamp": time.time()
}
# Register message queue
self.message_queue = self.message_bus.register_component(self.name)
logger.info(f"UDP Broadcast component initialized on port {broadcast_port}")
def initialize(self) -> bool:
"""Initialize UDP socket for broadcasting"""
try:
# Create UDP socket
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Subscribe to system status messages to get web server info
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
logger.info("UDP broadcast socket initialized successfully")
return True
except Exception as e:
logger.error(f"UDP broadcast initialization failed: {e}")
return False
def run(self):
"""Main broadcast loop"""
try:
logger.info("UDP broadcast thread started")
# Request initial web server status
self._request_web_server_status()
last_broadcast_time = 0
while self.running:
try:
current_time = time.time()
# Process messages
message = self.message_bus.get_message(self.name, timeout=1.0)
if message:
self._process_message(message)
# Broadcast every 30 seconds
if current_time - last_broadcast_time >= self.broadcast_interval:
self._broadcast_server_info()
last_broadcast_time = current_time
# Update heartbeat
self.heartbeat()
time.sleep(0.5)
except Exception as e:
logger.error(f"UDP broadcast loop error: {e}")
time.sleep(2.0)
except Exception as e:
logger.error(f"UDP broadcast run failed: {e}")
finally:
logger.info("UDP broadcast thread ended")
def shutdown(self):
"""Shutdown UDP broadcast component"""
try:
logger.info("Shutting down UDP broadcast...")
if self.socket:
self.socket.close()
self.socket = None
except Exception as e:
logger.error(f"UDP broadcast shutdown error: {e}")
def _process_message(self, message: Message):
"""Process received message"""
try:
if message.type == MessageType.SYSTEM_STATUS:
self._handle_system_status(message)
except Exception as e:
logger.error(f"Failed to process message: {e}")
def _handle_system_status(self, message: Message):
"""Handle system status messages from web dashboard"""
try:
if message.sender == "web_dashboard":
details = message.data.get("details", {})
if "host" in details and "port" in details:
self.server_info.update({
"host": details.get("host", "127.0.0.1"),
"port": details.get("port", 5001),
"ssl": details.get("ssl_enabled", False),
"url": details.get("url", "http://127.0.0.1:5001"),
"timestamp": time.time()
})
logger.debug(f"Updated server info from web dashboard: {self.server_info}")
except Exception as e:
logger.error(f"Failed to handle system status: {e}")
def _request_web_server_status(self):
"""Request current web server status"""
try:
# Send a status request to get current web server info
status_request = Message(
type=MessageType.CONFIG_REQUEST,
sender=self.name,
recipient="web_dashboard",
data={"section": "status"}
)
self.message_bus.publish(status_request)
except Exception as e:
logger.error(f"Failed to request web server status: {e}")
def _broadcast_server_info(self):
"""Broadcast server information via UDP"""
try:
if not self.socket:
logger.warning("UDP socket not available for broadcasting")
return
# Update timestamp
self.server_info["timestamp"] = time.time()
# Create broadcast message
broadcast_data = json.dumps(self.server_info, separators=(',', ':')).encode('utf-8')
# Broadcast to all interfaces
broadcast_addresses = self._get_broadcast_addresses()
for broadcast_addr in broadcast_addresses:
try:
self.socket.sendto(broadcast_data, (broadcast_addr, self.broadcast_port))
logger.debug(f"Broadcasted to {broadcast_addr}:{self.broadcast_port}")
except Exception as e:
logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}")
logger.info(f"UDP broadcast sent: {self.server_info['service']} at {self.server_info['url']}")
except Exception as e:
logger.error(f"UDP broadcast failed: {e}")
def _get_broadcast_addresses(self) -> list:
"""Get list of broadcast addresses for all network interfaces"""
broadcast_addresses = ['255.255.255.255'] # Global broadcast
try:
import netifaces
# Get all network interfaces
for interface in netifaces.interfaces():
try:
# Get IPv4 addresses for this interface
addrs = netifaces.ifaddresses(interface)
if netifaces.AF_INET in addrs:
for addr_info in addrs[netifaces.AF_INET]:
if 'broadcast' in addr_info:
broadcast_addr = addr_info['broadcast']
if broadcast_addr not in broadcast_addresses:
broadcast_addresses.append(broadcast_addr)
except Exception:
continue
except ImportError:
# netifaces not available, use basic approach
logger.debug("netifaces not available, using global broadcast only")
except Exception as e:
logger.debug(f"Error getting network interfaces: {e}")
return broadcast_addresses
def get_current_server_info(self) -> Dict[str, Any]:
"""Get current server information"""
return self.server_info.copy()
\ No newline at end of file
......@@ -28,6 +28,7 @@ python-dotenv>=0.19.0
psutil>=5.8.0
click>=8.0.0
watchdog>=3.0.0
netifaces>=0.11.0
# Video and image processing
opencv-python>=4.5.0
......
#!/usr/bin/env python3
"""
Setup script for MBetter Discovery Application
Creates a standalone executable for Windows and Linux
"""
import sys
import os
import subprocess
from pathlib import Path
def install_requirements():
"""Install required packages"""
requirements = [
"PyQt6>=6.4.0",
"netifaces>=0.11.0"
]
print("Installing required packages...")
for req in requirements:
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
print(f"✓ {req} installed")
except subprocess.CalledProcessError as e:
print(f"✗ Failed to install {req}: {e}")
return False
return True
def create_executable():
"""Create executable using PyInstaller with spec files"""
try:
# Check if PyInstaller is available
subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"])
print("Creating executable...")
# Determine platform and spec file
if sys.platform.startswith("win"):
spec_file = "mbetter_discovery_windows.spec"
platform_name = "Windows"
else:
spec_file = "mbetter_discovery_linux.spec"
platform_name = "Linux"
if not os.path.exists(spec_file):
print(f"✗ Spec file {spec_file} not found")
return False
print(f"Building for {platform_name} using {spec_file}...")
# PyInstaller command with spec file
cmd = [sys.executable, "-m", "PyInstaller", spec_file]
subprocess.check_call(cmd)
print("✓ Executable created in dist/ directory")
# Show executable location
if sys.platform.startswith("win"):
exe_path = "dist/MBetterDiscovery.exe"
else:
exe_path = "dist/MBetterDiscovery"
if os.path.exists(exe_path):
print(f"✓ Executable location: {exe_path}")
return True
except subprocess.CalledProcessError as e:
print(f"✗ Failed to create executable: {e}")
return False
def create_batch_file():
"""Create Windows batch file for easy running"""
batch_content = """@echo off
echo Starting MBetter Discovery...
python mbetter_discovery.py
if %errorlevel% neq 0 (
echo.
echo Error: Python or required packages not found
echo Please run setup_discovery.py first to install requirements
echo.
pause
)
"""
with open("run_discovery.bat", "w") as f:
f.write(batch_content)
print("✓ Created run_discovery.bat for Windows")
def create_shell_script():
"""Create Linux shell script for easy running"""
shell_content = """#!/bin/bash
echo "Starting MBetter Discovery..."
python3 mbetter_discovery.py
if [ $? -ne 0 ]; then
echo
echo "Error: Python3 or required packages not found"
echo "Please run: python3 setup_discovery.py"
echo
read -p "Press Enter to continue..."
fi
"""
with open("run_discovery.sh", "w") as f:
f.write(shell_content)
# Make executable
os.chmod("run_discovery.sh", 0o755)
print("✓ Created run_discovery.sh for Linux")
def main():
"""Main setup function"""
print("=" * 50)
print("MBetter Discovery Application Setup")
print("=" * 50)
# Install requirements
if not install_requirements():
print("\n✗ Setup failed - could not install requirements")
return False
# Create platform-specific run scripts
if sys.platform.startswith("win"):
create_batch_file()
else:
create_shell_script()
# Ask if user wants to create executable
create_exe = input("\nDo you want to create a standalone executable? (y/n): ").lower().strip()
if create_exe in ('y', 'yes'):
if create_executable():
print("\n✓ Setup completed successfully!")
print("You can run the executable from the dist/ directory")
else:
print("\n⚠ Setup completed with warnings - executable creation failed")
print("You can still run the application with the scripts created")
else:
print("\n✓ Setup completed successfully!")
if sys.platform.startswith("win"):
print("Run the application with: run_discovery.bat")
else:
print("Run the application with: ./run_discovery.sh")
return True
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nSetup interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n✗ Setup failed with error: {e}")
sys.exit(1)
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script for the Qt6 discovery application
Tests core functionality without requiring a display
"""
import sys
import json
import socket
import threading
import time
from unittest.mock import Mock, patch
def test_imports():
"""Test that all required modules can be imported"""
print("Testing imports...")
try:
import json
print("✓ json module")
import socket
print("✓ socket module")
import threading
print("✓ threading module")
import webbrowser
print("✓ webbrowser module")
from PyQt6.QtWidgets import QApplication
print("✓ PyQt6.QtWidgets")
from PyQt6.QtCore import QTimer, pyqtSignal, QObject, Qt, QThread
print("✓ PyQt6.QtCore")
print("✓ All imports successful")
return True
except ImportError as e:
print(f"✗ Import failed: {e}")
return False
def test_udp_worker_class():
"""Test UDPDiscoveryWorker class without GUI"""
print("\nTesting UDPDiscoveryWorker class...")
try:
# Import the worker class definition
import importlib.util
spec = importlib.util.spec_from_file_location("mbetter_discovery", "mbetter_discovery.py")
discovery_module = importlib.util.module_from_spec(spec)
# Mock PyQt6 to avoid GUI requirements
with patch.dict('sys.modules', {
'PyQt6.QtWidgets': Mock(),
'PyQt6.QtCore': Mock(QObject=object, pyqtSignal=Mock(), QThread=Mock()),
'PyQt6.QtGui': Mock()
}):
spec.loader.exec_module(discovery_module)
# Test worker class can be instantiated
worker = discovery_module.UDPDiscoveryWorker(45123)
print("✓ UDPDiscoveryWorker instantiated")
# Test basic properties
assert worker.listen_port == 45123
assert worker.running == False
print("✓ Worker properties correct")
return True
except Exception as e:
print(f"✗ UDPDiscoveryWorker test failed: {e}")
return False
def test_message_processing():
"""Test message processing logic"""
print("\nTesting message processing...")
try:
# Test valid MBetterClient message
valid_message = {
"service": "MBetterClient",
"host": "192.168.1.100",
"port": 5001,
"ssl": False,
"url": "http://192.168.1.100:5001",
"timestamp": time.time()
}
# Test JSON encoding/decoding (simulates UDP message processing)
json_data = json.dumps(valid_message)
decoded_message = json.loads(json_data)
# Validate message structure
if (isinstance(decoded_message, dict) and
decoded_message.get('service') == 'MBetterClient' and
'url' in decoded_message):
print("✓ Valid message structure detected")
else:
print("✗ Message validation failed")
return False
# Test invalid message
invalid_message = {"service": "OtherService", "url": "http://example.com"}
if not (invalid_message.get('service') == 'MBetterClient'):
print("✓ Invalid message correctly rejected")
return True
except Exception as e:
print(f"✗ Message processing test failed: {e}")
return False
def test_discovery_integration():
"""Test discovery integration with UDP broadcast"""
print("\nTesting discovery integration...")
def mock_listener():
"""Mock UDP listener that receives broadcasts"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 45124)) # Use different port to avoid conflicts
sock.settimeout(3.0)
print("Mock listener started on port 45124")
data, addr = sock.recvfrom(1024)
message = json.loads(data.decode('utf-8'))
if message.get('service') == 'MBetterClient':
print(f"✓ Received test broadcast: {message['url']}")
sock.close()
return True
else:
print("✗ Invalid broadcast received")
sock.close()
return False
except socket.timeout:
print("⚠ No broadcast received (timeout)")
sock.close()
return False
except Exception as e:
print(f"✗ Mock listener error: {e}")
return False
def send_test_broadcast():
"""Send test broadcast to mock listener"""
time.sleep(0.5) # Wait for listener to start
try:
test_message = {
"service": "MBetterClient",
"host": "127.0.0.1",
"port": 5001,
"ssl": False,
"url": "http://127.0.0.1:5001",
"timestamp": time.time()
}
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
data = json.dumps(test_message).encode('utf-8')
sock.sendto(data, ('127.0.0.1', 45124))
sock.close()
print("✓ Test broadcast sent")
except Exception as e:
print(f"✗ Failed to send test broadcast: {e}")
# Start mock listener in thread
listener_thread = threading.Thread(target=mock_listener)
listener_thread.daemon = True
listener_thread.start()
# Send test broadcast
sender_thread = threading.Thread(target=send_test_broadcast)
sender_thread.daemon = True
sender_thread.start()
# Wait for completion
listener_thread.join(timeout=4)
sender_thread.join(timeout=1)
if listener_thread.is_alive():
print("⚠ Integration test timed out")
return False
print("✓ Integration test completed")
return True
def main():
"""Main test function"""
print("=" * 60)
print("Qt6 Discovery Application Test")
print("=" * 60)
tests = [
("Module Imports", test_imports),
("UDPDiscoveryWorker", test_udp_worker_class),
("Message Processing", test_message_processing),
("Discovery Integration", test_discovery_integration),
]
results = []
for test_name, test_func in tests:
print(f"\n--- {test_name} Test ---")
try:
result = test_func()
results.append((test_name, result))
except Exception as e:
print(f"✗ {test_name} test crashed: {e}")
results.append((test_name, False))
# Summary
print("\n" + "=" * 60)
print("Test Results Summary")
print("=" * 60)
passed = 0
total = len(results)
for test_name, result in results:
status = "✓ PASS" if result else "✗ FAIL"
print(f"{test_name:<20} {status}")
if result:
passed += 1
print(f"\nOverall: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! Discovery application is ready for use.")
return True
else:
print("⚠ Some tests failed. Check the output above for details.")
return False
if __name__ == "__main__":
try:
success = main()
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n\nTest interrupted by user")
sys.exit(1)
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script for UDP broadcast functionality
"""
import socket
import json
import time
import threading
import sys
def test_udp_broadcast():
"""Test UDP broadcast functionality"""
print("Testing UDP broadcast functionality...")
# Test server info
server_info = {
"service": "MBetterClient",
"host": "127.0.0.1",
"port": 5001,
"ssl": False,
"url": "http://127.0.0.1:5001",
"timestamp": time.time()
}
try:
# Create UDP socket for broadcasting
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Broadcast data
broadcast_data = json.dumps(server_info).encode('utf-8')
print(f"Broadcasting: {server_info}")
# Send to localhost broadcast and general broadcast
addresses = [
('127.255.255.255', 45123), # Local broadcast
('255.255.255.255', 45123), # Global broadcast
]
for addr in addresses:
try:
sock.sendto(broadcast_data, addr)
print(f"✓ Sent to {addr}")
except Exception as e:
print(f"✗ Failed to send to {addr}: {e}")
sock.close()
print("✓ Broadcast test completed successfully")
return True
except Exception as e:
print(f"✗ Broadcast test failed: {e}")
return False
def test_udp_listener():
"""Test UDP listener functionality"""
print("\nTesting UDP listener functionality...")
def listen_for_broadcasts():
try:
# Create UDP socket for listening
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 45123))
sock.settimeout(5.0) # 5 second timeout
print("Listening for broadcasts on port 45123...")
while True:
try:
data, addr = sock.recvfrom(1024)
message = json.loads(data.decode('utf-8'))
if message.get('service') == 'MBetterClient':
print(f"✓ Received broadcast from {addr}: {message}")
sock.close()
return True
except socket.timeout:
print("⚠ Timeout - no broadcasts received")
sock.close()
return False
except Exception as e:
print(f"✗ Listener error: {e}")
sock.close()
return False
except Exception as e:
print(f"✗ Failed to create listener: {e}")
return False
# Start listener in separate thread
listener_thread = threading.Thread(target=listen_for_broadcasts)
listener_thread.daemon = True
listener_thread.start()
# Wait a moment then send test broadcast
time.sleep(1)
# Send test broadcast
test_udp_broadcast()
# Wait for listener to complete
listener_thread.join(timeout=6)
if listener_thread.is_alive():
print("✗ Listener test timed out")
return False
else:
print("✓ Listener test completed")
return True
def test_json_structure():
"""Test JSON message structure"""
print("\nTesting JSON message structure...")
# Test valid message
valid_message = {
"service": "MBetterClient",
"host": "192.168.1.100",
"port": 5001,
"ssl": True,
"url": "https://192.168.1.100:5001",
"timestamp": time.time()
}
try:
# Test JSON encoding/decoding
json_data = json.dumps(valid_message)
decoded_data = json.loads(json_data)
# Validate required fields
required_fields = ['service', 'host', 'port', 'ssl', 'url', 'timestamp']
missing_fields = [field for field in required_fields if field not in decoded_data]
if missing_fields:
print(f"✗ Missing required fields: {missing_fields}")
return False
if decoded_data['service'] != 'MBetterClient':
print(f"✗ Invalid service name: {decoded_data['service']}")
return False
print("✓ JSON structure validation passed")
print(f" Sample message: {json_data}")
return True
except Exception as e:
print(f"✗ JSON validation failed: {e}")
return False
def main():
"""Main test function"""
print("=" * 60)
print("UDP Broadcast System Test")
print("=" * 60)
tests = [
("JSON Structure", test_json_structure),
("UDP Broadcast", test_udp_broadcast),
("UDP Listener", test_udp_listener),
]
results = []
for test_name, test_func in tests:
print(f"\n--- {test_name} Test ---")
try:
result = test_func()
results.append((test_name, result))
except Exception as e:
print(f"✗ {test_name} test crashed: {e}")
results.append((test_name, False))
# Summary
print("\n" + "=" * 60)
print("Test Results Summary")
print("=" * 60)
passed = 0
total = len(results)
for test_name, result in results:
status = "✓ PASS" if result else "✗ FAIL"
print(f"{test_name:<20} {status}")
if result:
passed += 1
print(f"\nOverall: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! UDP broadcast system is working correctly.")
return True
else:
print("⚠ Some tests failed. Check the output above for details.")
return False
if __name__ == "__main__":
try:
success = main()
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n\nTest interrupted by user")
sys.exit(1)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment