overcode 0.1.0__py3-none-any.whl → 0.1.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.
- overcode/__init__.py +1 -1
- overcode/cli.py +42 -3
- overcode/config.py +49 -0
- overcode/daemon_logging.py +144 -0
- overcode/daemon_utils.py +84 -0
- overcode/history_reader.py +17 -5
- overcode/implementations.py +11 -0
- overcode/launcher.py +3 -0
- overcode/mocks.py +4 -0
- overcode/monitor_daemon.py +25 -126
- overcode/pid_utils.py +10 -3
- overcode/protocols.py +12 -0
- overcode/session_manager.py +3 -0
- overcode/settings.py +20 -1
- overcode/standing_instructions.py +15 -6
- overcode/status_constants.py +11 -0
- overcode/status_detector.py +38 -0
- overcode/status_patterns.py +12 -0
- overcode/supervisor_daemon.py +40 -171
- overcode/tui.py +326 -39
- overcode/tui_helpers.py +18 -0
- overcode/web_api.py +486 -2
- overcode/web_chartjs.py +32 -0
- overcode/web_server.py +355 -3
- overcode/web_server_runner.py +104 -0
- overcode/web_templates.py +1093 -0
- {overcode-0.1.0.dist-info → overcode-0.1.2.dist-info}/METADATA +13 -1
- overcode-0.1.2.dist-info/RECORD +45 -0
- {overcode-0.1.0.dist-info → overcode-0.1.2.dist-info}/WHEEL +1 -1
- overcode/daemon.py +0 -1184
- overcode/daemon_state.py +0 -113
- overcode-0.1.0.dist-info/RECORD +0 -43
- {overcode-0.1.0.dist-info → overcode-0.1.2.dist-info}/entry_points.txt +0 -0
- {overcode-0.1.0.dist-info → overcode-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.0.dist-info → overcode-0.1.2.dist-info}/top_level.txt +0 -0
overcode/web_server.py
CHANGED
|
@@ -6,13 +6,32 @@ Uses Python stdlib http.server - no additional dependencies required.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
9
10
|
import sys
|
|
11
|
+
from datetime import datetime
|
|
10
12
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
11
|
-
from
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Tuple
|
|
12
15
|
from urllib.parse import urlparse, parse_qs
|
|
13
16
|
|
|
14
|
-
from .
|
|
15
|
-
|
|
17
|
+
from .settings import (
|
|
18
|
+
get_web_server_pid_path,
|
|
19
|
+
get_web_server_port_path,
|
|
20
|
+
ensure_session_dir,
|
|
21
|
+
)
|
|
22
|
+
from .pid_utils import is_process_running, stop_process
|
|
23
|
+
from .web_templates import get_dashboard_html, get_analytics_html
|
|
24
|
+
from .web_api import (
|
|
25
|
+
get_status_data,
|
|
26
|
+
get_timeline_data,
|
|
27
|
+
get_health_data,
|
|
28
|
+
# Analytics API functions
|
|
29
|
+
get_analytics_sessions,
|
|
30
|
+
get_analytics_timeline,
|
|
31
|
+
get_analytics_stats,
|
|
32
|
+
get_analytics_daily,
|
|
33
|
+
get_time_presets,
|
|
34
|
+
)
|
|
16
35
|
|
|
17
36
|
|
|
18
37
|
class OvercodeHandler(BaseHTTPRequestHandler):
|
|
@@ -136,3 +155,336 @@ def run_server(
|
|
|
136
155
|
except KeyboardInterrupt:
|
|
137
156
|
print("\nShutting down...")
|
|
138
157
|
server.shutdown()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# Web Server Management (for TUI toggle)
|
|
162
|
+
# =============================================================================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _find_available_port(start_port: int = 8080, max_attempts: int = 10) -> int:
|
|
166
|
+
"""Find an available port starting from start_port."""
|
|
167
|
+
import socket
|
|
168
|
+
|
|
169
|
+
for i in range(max_attempts):
|
|
170
|
+
port = start_port + i
|
|
171
|
+
try:
|
|
172
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
173
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
174
|
+
s.bind(("127.0.0.1", port))
|
|
175
|
+
return port
|
|
176
|
+
except OSError:
|
|
177
|
+
continue
|
|
178
|
+
raise RuntimeError(f"Could not find available port in range {start_port}-{start_port + max_attempts}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def is_web_server_running(session: str) -> bool:
|
|
182
|
+
"""Check if the web server is running for the given session."""
|
|
183
|
+
pid_path = get_web_server_pid_path(session)
|
|
184
|
+
return is_process_running(pid_path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_web_server_url(session: str) -> Optional[str]:
|
|
188
|
+
"""Get the URL of the running web server for the session."""
|
|
189
|
+
if not is_web_server_running(session):
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
port_path = get_web_server_port_path(session)
|
|
193
|
+
if not port_path.exists():
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
port = int(port_path.read_text().strip())
|
|
198
|
+
return f"http://localhost:{port}"
|
|
199
|
+
except (ValueError, OSError):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _log_to_file(session: str, message: str) -> None:
|
|
204
|
+
"""Write a debug message to the web server log."""
|
|
205
|
+
from datetime import datetime
|
|
206
|
+
from .settings import get_session_dir
|
|
207
|
+
try:
|
|
208
|
+
log_path = get_session_dir(session) / "web_server.log"
|
|
209
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
with open(log_path, "a") as f:
|
|
211
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
212
|
+
f.write(f"[{timestamp}] [start_web_server] {message}\n")
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def start_web_server(session: str, port: int = 8080) -> Tuple[bool, str]:
|
|
218
|
+
"""Start the analytics web server for a session.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
session: tmux session name
|
|
222
|
+
port: Preferred port (will try alternatives if busy)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (success, message)
|
|
226
|
+
"""
|
|
227
|
+
_log_to_file(session, f"start_web_server called with port={port}")
|
|
228
|
+
|
|
229
|
+
if is_web_server_running(session):
|
|
230
|
+
url = get_web_server_url(session)
|
|
231
|
+
_log_to_file(session, f"Already running at {url}")
|
|
232
|
+
return False, f"Already running at {url}"
|
|
233
|
+
|
|
234
|
+
ensure_session_dir(session)
|
|
235
|
+
|
|
236
|
+
# Find an available port
|
|
237
|
+
try:
|
|
238
|
+
actual_port = _find_available_port(port)
|
|
239
|
+
_log_to_file(session, f"Found available port: {actual_port}")
|
|
240
|
+
except RuntimeError as e:
|
|
241
|
+
_log_to_file(session, f"Failed to find port: {e}")
|
|
242
|
+
return False, str(e)
|
|
243
|
+
|
|
244
|
+
# Start the server as a subprocess (works better with Textual TUI)
|
|
245
|
+
import subprocess
|
|
246
|
+
from .settings import get_session_dir
|
|
247
|
+
log_path = get_session_dir(session) / "web_server.log"
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
# Open log file in append mode for stderr
|
|
251
|
+
log_file = open(log_path, "a")
|
|
252
|
+
cmd = [sys.executable, "-m", "overcode.web_server_runner",
|
|
253
|
+
"--session", session, "--port", str(actual_port)]
|
|
254
|
+
_log_to_file(session, f"Starting subprocess: {' '.join(cmd)}")
|
|
255
|
+
proc = subprocess.Popen(
|
|
256
|
+
cmd,
|
|
257
|
+
stdout=subprocess.DEVNULL,
|
|
258
|
+
stderr=log_file,
|
|
259
|
+
start_new_session=True,
|
|
260
|
+
)
|
|
261
|
+
_log_to_file(session, f"Subprocess started with PID: {proc.pid}")
|
|
262
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
263
|
+
_log_to_file(session, f"Subprocess failed: {e}")
|
|
264
|
+
return False, f"Failed to start: {e}"
|
|
265
|
+
|
|
266
|
+
# Wait briefly for the server to start
|
|
267
|
+
import time
|
|
268
|
+
for i in range(10):
|
|
269
|
+
time.sleep(0.1)
|
|
270
|
+
if is_web_server_running(session):
|
|
271
|
+
url = get_web_server_url(session)
|
|
272
|
+
_log_to_file(session, f"Server started successfully at {url}")
|
|
273
|
+
return True, f"Started at {url}"
|
|
274
|
+
_log_to_file(session, f"Waiting for server... attempt {i+1}/10")
|
|
275
|
+
|
|
276
|
+
_log_to_file(session, "Server failed to start within timeout")
|
|
277
|
+
return False, "Failed to start web server"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def stop_web_server(session: str) -> Tuple[bool, str]:
|
|
281
|
+
"""Stop the analytics web server for a session.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
session: tmux session name
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Tuple of (success, message)
|
|
288
|
+
"""
|
|
289
|
+
pid_path = get_web_server_pid_path(session)
|
|
290
|
+
port_path = get_web_server_port_path(session)
|
|
291
|
+
|
|
292
|
+
if not is_process_running(pid_path):
|
|
293
|
+
# Clean up stale files
|
|
294
|
+
try:
|
|
295
|
+
pid_path.unlink(missing_ok=True)
|
|
296
|
+
port_path.unlink(missing_ok=True)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
return False, "Not running"
|
|
300
|
+
|
|
301
|
+
stopped = stop_process(pid_path)
|
|
302
|
+
|
|
303
|
+
# Clean up port file
|
|
304
|
+
try:
|
|
305
|
+
port_path.unlink(missing_ok=True)
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
if stopped:
|
|
310
|
+
return True, "Stopped"
|
|
311
|
+
else:
|
|
312
|
+
return False, "Failed to stop"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def toggle_web_server(session: str, port: int = 8080) -> Tuple[bool, str]:
|
|
316
|
+
"""Toggle the web server on/off for a session.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
session: tmux session name
|
|
320
|
+
port: Preferred port (used when starting)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Tuple of (is_now_running, message)
|
|
324
|
+
"""
|
|
325
|
+
if is_web_server_running(session):
|
|
326
|
+
success, msg = stop_web_server(session)
|
|
327
|
+
return False, msg
|
|
328
|
+
else:
|
|
329
|
+
success, msg = start_web_server(session, port)
|
|
330
|
+
return success, msg
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# =============================================================================
|
|
334
|
+
# Analytics Web Server (for `overcode web` historical analytics dashboard)
|
|
335
|
+
# =============================================================================
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class AnalyticsHandler(BaseHTTPRequestHandler):
|
|
339
|
+
"""HTTP request handler for analytics dashboard."""
|
|
340
|
+
|
|
341
|
+
# Set by run_analytics_server before starting
|
|
342
|
+
tmux_session: str = "agents"
|
|
343
|
+
|
|
344
|
+
def do_GET(self) -> None:
|
|
345
|
+
"""Handle GET requests."""
|
|
346
|
+
parsed = urlparse(self.path)
|
|
347
|
+
path = parsed.path
|
|
348
|
+
query = parse_qs(parsed.query)
|
|
349
|
+
|
|
350
|
+
# Parse time range from query params
|
|
351
|
+
start = self._parse_datetime(query.get("start", [None])[0])
|
|
352
|
+
end = self._parse_datetime(query.get("end", [None])[0])
|
|
353
|
+
|
|
354
|
+
if path == "/" or path == "/index.html":
|
|
355
|
+
self._serve_analytics_dashboard()
|
|
356
|
+
elif path == "/static/chart.min.js":
|
|
357
|
+
self._serve_chartjs()
|
|
358
|
+
elif path == "/api/analytics/sessions":
|
|
359
|
+
self._serve_json(get_analytics_sessions(start, end))
|
|
360
|
+
elif path == "/api/analytics/timeline":
|
|
361
|
+
self._serve_json(get_analytics_timeline(self.tmux_session, start, end))
|
|
362
|
+
elif path == "/api/analytics/stats":
|
|
363
|
+
self._serve_json(get_analytics_stats(self.tmux_session, start, end))
|
|
364
|
+
elif path == "/api/analytics/daily":
|
|
365
|
+
self._serve_json(get_analytics_daily(start, end))
|
|
366
|
+
elif path == "/api/analytics/presets":
|
|
367
|
+
self._serve_json(get_time_presets())
|
|
368
|
+
elif path == "/health":
|
|
369
|
+
self._serve_json(get_health_data())
|
|
370
|
+
else:
|
|
371
|
+
self.send_error(404, "Not Found")
|
|
372
|
+
|
|
373
|
+
def _parse_datetime(self, value: Optional[str]) -> Optional[datetime]:
|
|
374
|
+
"""Parse ISO datetime string from query param."""
|
|
375
|
+
if not value:
|
|
376
|
+
return None
|
|
377
|
+
try:
|
|
378
|
+
return datetime.fromisoformat(value)
|
|
379
|
+
except (ValueError, TypeError):
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def _serve_analytics_dashboard(self) -> None:
|
|
383
|
+
"""Serve the analytics dashboard HTML page."""
|
|
384
|
+
try:
|
|
385
|
+
html = get_analytics_html()
|
|
386
|
+
html_bytes = html.encode("utf-8")
|
|
387
|
+
self.send_response(200)
|
|
388
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
389
|
+
self.send_header("Content-Length", str(len(html_bytes)))
|
|
390
|
+
self.send_header("Cache-Control", "no-cache")
|
|
391
|
+
self.end_headers()
|
|
392
|
+
self.wfile.write(html_bytes)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
self.send_error(500, f"Internal error: {e}")
|
|
395
|
+
|
|
396
|
+
def _serve_chartjs(self) -> None:
|
|
397
|
+
"""Serve the embedded Chart.js library."""
|
|
398
|
+
try:
|
|
399
|
+
from .web_chartjs import CHARTJS_JS
|
|
400
|
+
js_bytes = CHARTJS_JS.encode("utf-8")
|
|
401
|
+
self.send_response(200)
|
|
402
|
+
self.send_header("Content-Type", "application/javascript")
|
|
403
|
+
self.send_header("Content-Length", str(len(js_bytes)))
|
|
404
|
+
# Cache for 1 year - it's a versioned static asset
|
|
405
|
+
self.send_header("Cache-Control", "public, max-age=31536000")
|
|
406
|
+
self.end_headers()
|
|
407
|
+
self.wfile.write(js_bytes)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
self.send_error(500, f"Internal error: {e}")
|
|
410
|
+
|
|
411
|
+
def _serve_json(self, data) -> None:
|
|
412
|
+
"""Serve JSON data."""
|
|
413
|
+
try:
|
|
414
|
+
body = json.dumps(data, indent=2, default=str)
|
|
415
|
+
body_bytes = body.encode("utf-8")
|
|
416
|
+
self.send_response(200)
|
|
417
|
+
self.send_header("Content-Type", "application/json")
|
|
418
|
+
self.send_header("Content-Length", str(len(body_bytes)))
|
|
419
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
420
|
+
self.send_header("Cache-Control", "no-cache")
|
|
421
|
+
self.end_headers()
|
|
422
|
+
self.wfile.write(body_bytes)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
self.send_error(500, f"Internal error: {e}")
|
|
425
|
+
|
|
426
|
+
def log_message(self, format: str, *args) -> None:
|
|
427
|
+
"""Custom log format - less verbose than default."""
|
|
428
|
+
if args and len(args) >= 2:
|
|
429
|
+
status = str(args[1])
|
|
430
|
+
path = str(args[0])
|
|
431
|
+
# Don't log successful API polls
|
|
432
|
+
if status.startswith("2") and "/api/" in path:
|
|
433
|
+
return
|
|
434
|
+
sys.stderr.write(f"[analytics] {args[0] if args else format}\n")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def run_analytics_server(
|
|
438
|
+
host: str = "127.0.0.1",
|
|
439
|
+
port: int = 8080,
|
|
440
|
+
tmux_session: str = "agents",
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Run the analytics web dashboard server.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
host: Host to bind to (default: 127.0.0.1 for local only)
|
|
446
|
+
port: Port to listen on (default: 8080)
|
|
447
|
+
tmux_session: tmux session name for session-specific data
|
|
448
|
+
"""
|
|
449
|
+
# Set the tmux session on the handler class
|
|
450
|
+
AnalyticsHandler.tmux_session = tmux_session
|
|
451
|
+
|
|
452
|
+
server_address = (host, port)
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
server = HTTPServer(server_address, AnalyticsHandler)
|
|
456
|
+
except OSError as e:
|
|
457
|
+
if "Address already in use" in str(e):
|
|
458
|
+
print(f"Error: Port {port} is already in use. Try a different port with --port")
|
|
459
|
+
sys.exit(1)
|
|
460
|
+
raise
|
|
461
|
+
|
|
462
|
+
# Get actual bound address for display
|
|
463
|
+
bound_host, bound_port = server.server_address
|
|
464
|
+
|
|
465
|
+
print(f"Overcode Analytics Dashboard")
|
|
466
|
+
print(f"=============================")
|
|
467
|
+
print(f"")
|
|
468
|
+
print(f"Local: http://localhost:{bound_port}")
|
|
469
|
+
|
|
470
|
+
if host == "0.0.0.0":
|
|
471
|
+
# Try to get the machine's IP for network access
|
|
472
|
+
try:
|
|
473
|
+
import socket
|
|
474
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
475
|
+
s.connect(("8.8.8.8", 80))
|
|
476
|
+
ip = s.getsockname()[0]
|
|
477
|
+
s.close()
|
|
478
|
+
print(f"Network: http://{ip}:{bound_port}")
|
|
479
|
+
except Exception:
|
|
480
|
+
print(f"Network: http://<your-ip>:{bound_port}")
|
|
481
|
+
|
|
482
|
+
print(f"")
|
|
483
|
+
print(f"Press Ctrl+C to stop")
|
|
484
|
+
print(f"")
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
server.serve_forever()
|
|
488
|
+
except KeyboardInterrupt:
|
|
489
|
+
print("\nShutting down...")
|
|
490
|
+
server.shutdown()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Web server runner - standalone script for running the analytics server.
|
|
4
|
+
|
|
5
|
+
This is invoked as a subprocess from the TUI to avoid multiprocessing issues
|
|
6
|
+
with Textual's file descriptor management.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
import traceback
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from http.server import HTTPServer
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_log_path(session: str) -> Path:
|
|
20
|
+
"""Get path for web server log file."""
|
|
21
|
+
from .settings import get_session_dir
|
|
22
|
+
return get_session_dir(session) / "web_server.log"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def log(session: str, message: str) -> None:
|
|
26
|
+
"""Write a log message to the web server log file."""
|
|
27
|
+
try:
|
|
28
|
+
log_path = get_log_path(session)
|
|
29
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
with open(log_path, "a") as f:
|
|
31
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
32
|
+
f.write(f"[{timestamp}] {message}\n")
|
|
33
|
+
except Exception:
|
|
34
|
+
pass # Can't log, silently fail
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main():
|
|
38
|
+
parser = argparse.ArgumentParser(description="Run Overcode analytics web server")
|
|
39
|
+
parser.add_argument("--session", "-s", required=True, help="Session name")
|
|
40
|
+
parser.add_argument("--port", "-p", type=int, default=8080, help="Port to listen on")
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
session = args.session
|
|
44
|
+
port = args.port
|
|
45
|
+
|
|
46
|
+
log(session, f"Starting web server on port {port}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from .settings import get_web_server_pid_path, get_web_server_port_path
|
|
50
|
+
from .pid_utils import write_pid_file
|
|
51
|
+
|
|
52
|
+
pid_path = get_web_server_pid_path(session)
|
|
53
|
+
port_path = get_web_server_port_path(session)
|
|
54
|
+
|
|
55
|
+
# Write PID file
|
|
56
|
+
write_pid_file(pid_path)
|
|
57
|
+
log(session, f"Wrote PID file: {pid_path}")
|
|
58
|
+
|
|
59
|
+
# Write port file so TUI can find the URL
|
|
60
|
+
port_path.write_text(str(port))
|
|
61
|
+
log(session, f"Wrote port file: {port_path}")
|
|
62
|
+
|
|
63
|
+
def cleanup(signum, frame):
|
|
64
|
+
log(session, f"Received signal {signum}, shutting down")
|
|
65
|
+
try:
|
|
66
|
+
pid_path.unlink(missing_ok=True)
|
|
67
|
+
port_path.unlink(missing_ok=True)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
sys.exit(0)
|
|
71
|
+
|
|
72
|
+
signal.signal(signal.SIGTERM, cleanup)
|
|
73
|
+
signal.signal(signal.SIGINT, cleanup)
|
|
74
|
+
|
|
75
|
+
# Import here to avoid circular imports
|
|
76
|
+
from .web_server import AnalyticsHandler
|
|
77
|
+
|
|
78
|
+
server_address = ("127.0.0.1", port)
|
|
79
|
+
log(session, f"Creating HTTP server at {server_address}")
|
|
80
|
+
server = HTTPServer(server_address, AnalyticsHandler)
|
|
81
|
+
log(session, "Server created, starting serve_forever()")
|
|
82
|
+
|
|
83
|
+
# Redirect stdout/stderr AFTER setup is complete
|
|
84
|
+
sys.stdout = open(os.devnull, 'w')
|
|
85
|
+
sys.stderr = open(os.devnull, 'w')
|
|
86
|
+
|
|
87
|
+
server.serve_forever()
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
log(session, f"ERROR: {e}\n{traceback.format_exc()}")
|
|
91
|
+
# Clean up on error
|
|
92
|
+
try:
|
|
93
|
+
from .settings import get_web_server_pid_path, get_web_server_port_path
|
|
94
|
+
pid_path = get_web_server_pid_path(session)
|
|
95
|
+
port_path = get_web_server_port_path(session)
|
|
96
|
+
pid_path.unlink(missing_ok=True)
|
|
97
|
+
port_path.unlink(missing_ok=True)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|