-
-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathstart.py
More file actions
149 lines (127 loc) · 5.22 KB
/
Copy pathstart.py
File metadata and controls
149 lines (127 loc) · 5.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/usr/bin/env python3
"""
LED Matrix Web Interface V3 Startup Script
Modern web interface with real-time display preview and plugin management.
"""
import os
import socket
import subprocess
import sys
import logging
from pathlib import Path
logger = logging.getLogger('web_interface.start')
# No route to host, broken pipe, connection reset: a client went away.
_CLIENT_DISCONNECT_ERRNOS = (113, 32, 104)
def _exc_info_value(exc_info):
"""The exception a logging call's ``exc_info`` argument refers to.
logging accepts a (type, value, traceback) tuple, an exception instance,
or True for the exception being handled. The werkzeug filter below used
to unpack it as a tuple, so True or an instance raised TypeError from
inside the logging call itself.
"""
if isinstance(exc_info, BaseException):
return exc_info
if isinstance(exc_info, tuple):
return exc_info[1] if len(exc_info) > 1 else None
return sys.exc_info()[1]
def get_local_ips():
"""Get list of local IP addresses the service will be accessible on."""
ips = []
# Check if AP mode is active
try:
result = subprocess.run(
["systemctl", "is-active", "hostapd"],
capture_output=True,
text=True,
timeout=2
)
if result.returncode == 0 and result.stdout.strip() == "active":
ips.append("192.168.4.1 (AP Mode)")
except Exception: # nosec B110 - AP mode IP detection is non-critical startup info; systemctl may not exist
pass
# Get IPs from hostname -I
try:
result = subprocess.run(
["hostname", "-I"],
capture_output=True,
text=True,
timeout=2
)
if result.returncode == 0:
for ip in result.stdout.strip().split():
ip = ip.strip()
if ip and not ip.startswith("127.") and ip != "192.168.4.1":
ips.append(ip)
except Exception: # nosec B110 - hostname -I output parsing; non-critical startup info
pass
# Fallback: try socket method
if not ips:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
if ip and not ip.startswith("127."):
ips.append(ip)
finally:
s.close()
except Exception:
pass
return ips if ips else ["localhost"]
def main():
"""Main startup function."""
# Change to project root directory
project_root = Path(__file__).parent.parent
os.chdir(project_root)
# Add to Python path
sys.path.insert(0, str(project_root))
# Configure logging to suppress non-critical socket errors
# These occur when clients disconnect and are harmless
werkzeug_logger = logging.getLogger('werkzeug')
original_log_exception = werkzeug_logger.error
def log_exception_filtered(message, *args, **kwargs):
"""Filter out non-critical socket errors from werkzeug logs."""
if isinstance(message, str):
# Suppress "No route to host" and similar connection errors
if 'No route to host' in message or 'errno 113' in message:
# Log at debug level instead of error
werkzeug_logger.debug(message, *args, **kwargs)
return
# Suppress broken pipe errors (client disconnected)
if 'Broken pipe' in message or 'errno 32' in message:
werkzeug_logger.debug(message, *args, **kwargs)
return
# For exceptions, check if it's a socket error
if 'exc_info' in kwargs and kwargs['exc_info']:
exc_value = _exc_info_value(kwargs['exc_info'])
if isinstance(exc_value, OSError):
if exc_value.errno in _CLIENT_DISCONNECT_ERRNOS:
werkzeug_logger.debug(message, *args, **kwargs)
return
# Log everything else normally
original_log_exception(message, *args, **kwargs)
werkzeug_logger.error = log_exception_filtered
# Importing the app also sets up logging, so the lines below reach the
# journal through it.
from web_interface.app import app, start_auto_update_scheduler
start_auto_update_scheduler()
logger.info("Starting LED Matrix Web Interface V3, binding to 0.0.0.0:5000")
# get_local_ips() always returns at least "localhost".
logger.info("Access the interface at:")
for ip in get_local_ips():
if "AP Mode" in ip:
logger.info(" - http://192.168.4.1:5000 (AP Mode - connect to LEDMatrix-Setup WiFi)")
else:
logger.info(" - http://%s:5000", ip)
try:
# threaded=True is Flask's default since 1.0, but set it explicitly
# so it's self-documenting: the three /api/v3/stream/* SSE endpoints
# hold long-lived connections and would starve other requests under
# a single-threaded server.
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)
except OSError as e:
if e.errno not in _CLIENT_DISCONNECT_ERRNOS:
raise
werkzeug_logger.debug("Client disconnected: %s", e, exc_info=True)
if __name__ == '__main__':
main()