Add Windows Player support and related files
- New windows/ directory with build scripts, specs, and configuration - Windows-specific requirements (requirements_win.txt) - Launch and runtime scripts for Windows (run_win.py, launch_player.bat) - PyInstaller build configuration (build.spec) - Updated .gitignore to exclude windows/venv312/ - Updated config and source files for Windows compatibility - Moved working_files to proper directory
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
"""
|
||||
Kiwy Signage Player - Windows Entry Point
|
||||
------------------------------------------
|
||||
Patches platform-specific code and environment for Windows before launching
|
||||
the original Kivy-based signage player application.
|
||||
|
||||
Usage:
|
||||
python run_win.py (for development/testing)
|
||||
run_win.exe (after PyInstaller build)
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import tempfile
|
||||
import subprocess
|
||||
import shutil
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _show_error_box(title, message):
|
||||
"""Show a Windows message box with the error (visible even without console)."""
|
||||
try:
|
||||
ctypes.windll.user32.MessageBoxW(0, message, title, 0x10) # MB_ICONERROR
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Ensure we are on Windows; warn if not ---
|
||||
if platform.system() != 'Windows':
|
||||
print(f"WARNING: This entry point is designed for Windows. Detected: {platform.system()}")
|
||||
|
||||
# =====================================================================
|
||||
# 1. Set Windows-compatible environment variables BEFORE Kivy imports
|
||||
# =====================================================================
|
||||
|
||||
# Video driver: Use 'windib' or 'angle' (DirectX via ANGLE) for Windows
|
||||
os.environ.setdefault('SDL_VIDEODRIVER', 'windows')
|
||||
# Audio driver: DirectSound for Windows
|
||||
os.environ.setdefault('SDL_AUDIODRIVER', 'directsound')
|
||||
# Prevent screensaver
|
||||
os.environ.setdefault('SDL_VIDEO_ALLOW_SCREENSAVER', '0')
|
||||
# Video backend via ffpyplayer
|
||||
os.environ.setdefault('KIVY_VIDEO', 'ffpyplayer')
|
||||
os.environ.setdefault('KIVY_AUDIO', 'ffpyplayer')
|
||||
os.environ.setdefault('FFPYPLAYER_CODECS', 'h264,h265,vp9,vp8')
|
||||
# Kivy window backend: prefer SDL2 on Windows
|
||||
os.environ.setdefault('KIVY_WINDOW', 'sdl2')
|
||||
# OpenGL
|
||||
os.environ.setdefault('KIVY_GL_BACKEND', 'angle_sdl2')
|
||||
|
||||
# =====================================================================
|
||||
# 2. Patch the evdev import — it is Linux-only. We provide a dummy
|
||||
# module so that `from evdev import ...` will not crash on Windows.
|
||||
# =====================================================================
|
||||
class _DummyEvdev:
|
||||
"""Fake evdev module that raises ImportError for all meaningful uses."""
|
||||
|
||||
class InputDevice:
|
||||
def __init__(self, *a, **kw):
|
||||
raise ImportError("evdev is not available on Windows")
|
||||
|
||||
class ecodes:
|
||||
EV_KEY = 1
|
||||
EV_ABS = 3
|
||||
|
||||
def categorize(self, *a, **kw):
|
||||
raise ImportError("evdev is not available on Windows")
|
||||
|
||||
def list_devices(self):
|
||||
return []
|
||||
|
||||
|
||||
class _FakeEvdevInputDevice:
|
||||
pass
|
||||
|
||||
|
||||
# Inject the fake evdev module into sys.modules so that main.py's
|
||||
# `try: import evdev` succeeds but EVDEV_AVAILABLE stays False.
|
||||
_evdev_dummy = _DummyEvdev()
|
||||
sys.modules['evdev'] = _evdev_dummy
|
||||
sys.modules['evdev.InputDevice'] = _FakeEvdevInputDevice
|
||||
|
||||
# =====================================================================
|
||||
# 3. Provide a Windows implementation of screen activity signaling
|
||||
# We monkey-patch the SignagePlayer.signal_screen_activity method
|
||||
# after the class is defined but before it's used, by hooking into
|
||||
# the import machinery.
|
||||
# =====================================================================
|
||||
|
||||
# We'll store a reference to the original module's signal_screen_activity
|
||||
# so we can replace it after import. This is done inside _patch_main().
|
||||
|
||||
def _windows_screen_activity(self, dt):
|
||||
"""Windows alternative to Linux screen-keep-awake commands.
|
||||
|
||||
Uses SetThreadExecutionState via ctypes to tell Windows to keep
|
||||
the display and system awake.
|
||||
"""
|
||||
try:
|
||||
# ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED | ES_CONTINUOUS
|
||||
ES_CONTINUOUS = 0x80000000
|
||||
ES_SYSTEM_REQUIRED = 0x00000001
|
||||
ES_DISPLAY_REQUIRED = 0x00000002
|
||||
|
||||
ctypes.windll.kernel32.SetThreadExecutionState(
|
||||
ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED
|
||||
)
|
||||
except Exception:
|
||||
pass # non-critical
|
||||
|
||||
|
||||
def _windows_find_browser():
|
||||
"""Find Chrome or Edge executable on Windows for weblink support.
|
||||
|
||||
Returns the path to the browser or None.
|
||||
"""
|
||||
# Common install locations
|
||||
candidates = [
|
||||
# Chrome
|
||||
os.path.expandvars(r'%PROGRAMFILES%\Google\Chrome\Application\chrome.exe'),
|
||||
os.path.expandvars(r'%PROGRAMFILES(X86)%\Google\Chrome\Application\chrome.exe'),
|
||||
os.path.expandvars(r'%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe'),
|
||||
# Edge
|
||||
os.path.expandvars(r'%PROGRAMFILES%\Microsoft\Edge\Application\msedge.exe'),
|
||||
os.path.expandvars(r'%PROGRAMFILES(X86)%\Microsoft\Edge\Application\msedge.exe'),
|
||||
os.path.expandvars(r'%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe'),
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
# Fallback: try PATH
|
||||
which = shutil.which('chrome') or shutil.which('msedge') or shutil.which('google-chrome')
|
||||
if which:
|
||||
return which
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _patch_main():
|
||||
"""Patch the main module after import for Windows compatibility."""
|
||||
# ── CRITICAL: Override Linux env vars BEFORE importing main ─────
|
||||
# main.py's top-level code sets SDL_VIDEODRIVER=wayland,x11,dummy
|
||||
# and other Linux values. We MUST override these before main.py
|
||||
# gets imported, otherwise Kivy will initialize with the wrong
|
||||
# window provider and crash with SystemExit(1).
|
||||
os.environ['SDL_VIDEODRIVER'] = 'windows'
|
||||
os.environ['SDL_AUDIODRIVER'] = 'directsound'
|
||||
os.environ['KIVY_WINDOW'] = 'sdl2'
|
||||
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
|
||||
os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect
|
||||
os.environ['KIVY_VIDEO'] = 'ffpyplayer'
|
||||
os.environ['KIVY_AUDIO'] = 'ffpyplayer'
|
||||
|
||||
# Now safe to import main.py — env vars are already Windows-correct
|
||||
import main as signage_main
|
||||
|
||||
# Replace signal_screen_activity
|
||||
# IMPORTANT: Assign under BOTH the attribute name AND the function's own name.
|
||||
# Kivy's WeakMethod stores self.__func__.__name__ (= '_windows_screen_activity')
|
||||
# and later does getattr(instance, '_windows_screen_activity'). If we only
|
||||
# assign under 'signal_screen_activity', the weakref lookup fails with
|
||||
# AttributeError: 'SignagePlayer' object has no attribute '_windows_screen_activity'
|
||||
signage_main.SignagePlayer.signal_screen_activity = _windows_screen_activity
|
||||
signage_main.SignagePlayer._windows_screen_activity = _windows_screen_activity
|
||||
|
||||
# Store a reference to the original play_weblink so we can wrap it
|
||||
_original_play_weblink = signage_main.SignagePlayer.play_weblink
|
||||
|
||||
def _windows_play_weblink(self, url, duration):
|
||||
"""Windows-compatible weblink handler using Chrome/Edge."""
|
||||
browser = _windows_find_browser()
|
||||
if not browser:
|
||||
from kivy.logger import Logger
|
||||
Logger.error(
|
||||
"SignagePlayer: Chrome/Edge not found. "
|
||||
"Cannot display weblink on Windows."
|
||||
)
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
from urllib.parse import urlparse
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
from kivy.logger import Logger
|
||||
Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}")
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
import subprocess
|
||||
from kivy.logger import Logger
|
||||
from kivy.clock import Clock
|
||||
|
||||
try:
|
||||
Logger.info(f"SignagePlayer: Opening weblink: {url} (browser: {browser})")
|
||||
self._kill_weblink_preload()
|
||||
|
||||
# Launch in fullscreen / kiosk-like mode
|
||||
self._weblink_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--kiosk',
|
||||
'--no-first-run',
|
||||
'--noerrdialogs',
|
||||
'--disable-infobars',
|
||||
'--incognito',
|
||||
'--no-default-browser-check',
|
||||
'--disable-session-crashed-bubble',
|
||||
'--disable-features=TranslateUI',
|
||||
url,
|
||||
], shell=False)
|
||||
|
||||
Clock.unschedule(self.next_media)
|
||||
self._start_inactivity_watchdog(duration)
|
||||
self.preload_next_media()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
Logger.error(f"SignagePlayer: Error opening weblink: {e}")
|
||||
self.consecutive_errors += 1
|
||||
self._skip_to_next_media()
|
||||
return False
|
||||
|
||||
# Replace weblink handling
|
||||
signage_main.SignagePlayer.play_weblink = _windows_play_weblink
|
||||
|
||||
# Patch the _get_browser_target_size to always return a reasonable size on Windows
|
||||
def _windows_get_browser_target_size(self):
|
||||
try:
|
||||
from kivy.core.window import Window
|
||||
width, height = Window.size
|
||||
if width >= 1280 and height >= 720:
|
||||
return int(width), int(height)
|
||||
except Exception:
|
||||
pass
|
||||
return 1920, 1080
|
||||
|
||||
signage_main.SignagePlayer._get_browser_target_size = _windows_get_browser_target_size
|
||||
|
||||
# Patch _start_inactivity_watchdog for Windows — /dev/input does not exist
|
||||
_original_watchdog = signage_main.SignagePlayer._start_inactivity_watchdog
|
||||
|
||||
def _windows_watchdog(self, duration):
|
||||
"""Windows watchdog: uses a simple timer since /dev/input is not available.
|
||||
|
||||
Falls back to a fixed timer that advances after 'duration' seconds.
|
||||
"""
|
||||
from kivy.clock import Clock
|
||||
import threading
|
||||
|
||||
self._stop_inactivity_watchdog()
|
||||
stop_event = threading.Event()
|
||||
self._watchdog_stop = stop_event
|
||||
weblink_proc = self._weblink_proc
|
||||
|
||||
def watchdog():
|
||||
import time
|
||||
# Simply wait for the duration, checking if Chromium exited early
|
||||
elapsed = 0.0
|
||||
step = 0.5
|
||||
while elapsed < duration and not stop_event.is_set():
|
||||
if weblink_proc is not None and weblink_proc.poll() is not None:
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
return
|
||||
time.sleep(step)
|
||||
elapsed += step
|
||||
if not stop_event.is_set():
|
||||
Clock.schedule_once(self.next_media, 0)
|
||||
|
||||
self._weblink_watchdog_thread = threading.Thread(
|
||||
target=watchdog, daemon=True, name='weblink-watchdog-win'
|
||||
)
|
||||
self._weblink_watchdog_thread.start()
|
||||
|
||||
signage_main.SignagePlayer._start_inactivity_watchdog = _windows_watchdog
|
||||
|
||||
# Patch the _kill_weblink_after_frame to work without Linux-specific code
|
||||
def _windows_kill_weblink_after_frame(self):
|
||||
from kivy.clock import Clock
|
||||
self._stop_inactivity_watchdog()
|
||||
self._kill_weblink_preload()
|
||||
proc = self._weblink_proc
|
||||
self._weblink_proc = None
|
||||
if proc is not None and proc.poll() is None:
|
||||
def _do_kill(dt):
|
||||
if proc.poll() is None:
|
||||
try:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
Clock.schedule_once(_do_kill, 0)
|
||||
signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame
|
||||
|
||||
# Patch _prewarm_weblink for Windows
|
||||
def _windows_prewarm_weblink(self, url):
|
||||
from urllib.parse import urlparse
|
||||
if not url:
|
||||
return
|
||||
scheme = urlparse(url).scheme.lower()
|
||||
if scheme not in ('http', 'https'):
|
||||
return
|
||||
browser = _windows_find_browser()
|
||||
if not browser:
|
||||
return
|
||||
import subprocess
|
||||
from kivy.logger import Logger
|
||||
self._kill_weblink_preload()
|
||||
try:
|
||||
Logger.debug(f"SignagePlayer: Pre-warming weblink off-screen: {url}")
|
||||
self._weblink_preload_proc = subprocess.Popen([
|
||||
browser,
|
||||
'--incognito',
|
||||
'--no-first-run',
|
||||
'--window-position=-9999,-9999',
|
||||
url,
|
||||
], shell=False)
|
||||
except Exception as exc:
|
||||
Logger.debug(f"SignagePlayer: Pre-warm failed (non-fatal): {exc}")
|
||||
self._weblink_preload_proc = None
|
||||
signage_main.SignagePlayer._prewarm_weblink = _windows_prewarm_weblink
|
||||
|
||||
# Patch cleanup of temp auth file (was using /tmp/)
|
||||
_original_connection_test = signage_main.SettingsPopup.test_connection
|
||||
|
||||
def _windows_test_connection(self):
|
||||
"""Test connection with Windows-safe temp file path."""
|
||||
import tempfile as _tf
|
||||
import os as _os
|
||||
from player_auth import PlayerAuth
|
||||
import re
|
||||
import threading
|
||||
from kivy.clock import Clock
|
||||
|
||||
self.ids.connection_status.text = 'Testing connection...'
|
||||
self.ids.connection_status.color = (1, 0.7, 0, 1)
|
||||
|
||||
def run_test():
|
||||
try:
|
||||
server_ip = self.ids.server_input.text.strip()
|
||||
screen_name = self.ids.screen_input.text.strip()
|
||||
quickconnect = self.ids.quickconnect_input.text.strip()
|
||||
port = self.ids.port_input.text.strip() or self.player.config.get('port', '')
|
||||
use_https = self.player.config.get('use_https', True)
|
||||
verify_ssl = self.player.config.get('verify_ssl', True)
|
||||
|
||||
if not all([server_ip, screen_name, quickconnect]):
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status('Error: Fill all fields', False))
|
||||
return
|
||||
|
||||
if server_ip.startswith('http://') or server_ip.startswith('https://'):
|
||||
server_url = server_ip
|
||||
if ':' not in server_ip.replace('https://', '').replace('http://', ''):
|
||||
if port and port not in ('443', '80'):
|
||||
server_url = f"{server_ip}:{port}"
|
||||
else:
|
||||
protocol = "https" if use_https else "http"
|
||||
if ':' in server_ip:
|
||||
server_url = f"{protocol}://{server_ip}"
|
||||
else:
|
||||
server_url = f"{protocol}://{server_ip}:{port}" if port else f"{protocol}://{server_ip}"
|
||||
|
||||
# Use Windows temp path
|
||||
temp_file = _os.path.join(_tf.gettempdir(), 'temp_auth_test.json')
|
||||
auth = PlayerAuth(temp_file, use_https=use_https, verify_ssl=verify_ssl)
|
||||
success, error = auth.authenticate(server_url=server_url, hostname=screen_name, quickconnect_code=quickconnect)
|
||||
|
||||
try:
|
||||
if _os.path.exists(temp_file):
|
||||
_os.remove(temp_file)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if success:
|
||||
player_name = auth.get_player_name()
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status(f'✓ Connected: {player_name}', True))
|
||||
else:
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status(f'✗ Failed: {error}', False))
|
||||
|
||||
except Exception as e:
|
||||
Clock.schedule_once(lambda dt: self.update_connection_status(f'✗ Error: {str(e)}', False))
|
||||
|
||||
threading.Thread(target=run_test, daemon=True).start()
|
||||
|
||||
signage_main.SettingsPopup.test_connection = _windows_test_connection
|
||||
|
||||
return signage_main
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 4. Adjust path so we can import the src modules
|
||||
# =====================================================================
|
||||
# When run from PyInstaller .exe: the runtime hook inserts paths.
|
||||
# When run as plain python, we add src/ relative to this file.
|
||||
_script_dir = Path(__file__).resolve().parent
|
||||
_project_root = _script_dir.parent
|
||||
_src_dir = _project_root / 'src'
|
||||
|
||||
for p in [str(_src_dir), str(_project_root)]:
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
# =====================================================================
|
||||
# 5. Determine the local data directory (next to the executable)
|
||||
# =====================================================================
|
||||
# The pyi_runtime_hook.py (when packaged) sets KIWY_DATA_DIR.
|
||||
# When running in dev mode from python, use the project root.
|
||||
# The executable will create its own local folders for playlist,
|
||||
# media, config, and logs where the executable is launched.
|
||||
DATA_DIR = os.environ.get('KIWY_DATA_DIR', str(_project_root))
|
||||
|
||||
# Create local data folders NEXT TO the executable
|
||||
os.makedirs(os.path.join(DATA_DIR, 'config', 'resources'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'media'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'media', 'edited_media'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'playlists'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'logs'), exist_ok=True)
|
||||
os.makedirs(os.path.join(DATA_DIR, 'config', 'certs'), exist_ok=True)
|
||||
|
||||
# =====================================================================
|
||||
# 6. Set Kivy config BEFORE importing Kivy
|
||||
# =====================================================================
|
||||
os.environ['KIVY_NO_FILELOG'] = '1' # Avoid file logging issues on Windows
|
||||
os.environ['KIVY_HOME'] = os.path.join(DATA_DIR, '.kivy')
|
||||
|
||||
from kivy.config import Config
|
||||
Config.set('kivy', 'keyboard_mode', '') # Disable default virtual keyboard
|
||||
Config.set('graphics', 'fullscreen', '0')
|
||||
Config.set('graphics', 'window_state', 'maximized')
|
||||
Config.set('graphics', 'multisampling', '0')
|
||||
Config.set('graphics', 'fast_rgba', '1')
|
||||
Config.set('kivy', 'log_level', 'warning')
|
||||
|
||||
# =====================================================================
|
||||
# 7. Patch the main module, then run the app
|
||||
# =====================================================================
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
# Write a startup marker so we know the .exe at least launched
|
||||
try:
|
||||
os.makedirs(os.path.join(DATA_DIR, 'logs'), exist_ok=True)
|
||||
marker = os.path.join(DATA_DIR, 'logs', 'startup_marker.txt')
|
||||
with open(marker, 'w') as f:
|
||||
f.write(f"run_win.py started at {__import__('time').time()}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Apply all Windows patches before launching
|
||||
try:
|
||||
patched_main = _patch_main()
|
||||
except Exception as e:
|
||||
# Catch early import errors (pre-Logger) to a file
|
||||
import traceback
|
||||
try:
|
||||
err_log = os.path.join(DATA_DIR, 'logs', 'startup_error.log')
|
||||
with open(err_log, 'w') as f:
|
||||
f.write(f"Error in _patch_main(): {e}\n")
|
||||
traceback.print_exc(file=f)
|
||||
except Exception:
|
||||
pass
|
||||
raise # Re-raise so console shows it too
|
||||
|
||||
from kivy.logger import Logger
|
||||
Logger.info("=" * 80)
|
||||
Logger.info("Kiwy Signage Player - Windows Edition")
|
||||
Logger.info(f"Python: {sys.version}")
|
||||
Logger.info(f"Platform: {platform.platform()}")
|
||||
Logger.info(f"Data directory: {DATA_DIR}")
|
||||
Logger.info("=" * 80)
|
||||
|
||||
# Patch base_dir in SignagePlayer instances to point to local data folder
|
||||
_original_init = patched_main.SignagePlayer.__init__
|
||||
|
||||
def _patched_init(self, **kwargs):
|
||||
"""Override SignagePlayer.__init__ to use local data folders.
|
||||
|
||||
Creates all necessary folders (config, media, playlists, logs)
|
||||
in the same directory where the executable is launched.
|
||||
"""
|
||||
# Call parent Widget.__init__
|
||||
_original_init(self, **kwargs)
|
||||
# Now override ALL paths to point to local data directory
|
||||
# (where the .exe is located)
|
||||
self.base_dir = DATA_DIR
|
||||
self.config_dir = os.path.join(DATA_DIR, 'config')
|
||||
self.media_dir = os.path.join(DATA_DIR, 'media')
|
||||
self.playlists_dir = os.path.join(DATA_DIR, 'playlists')
|
||||
self.config_file = os.path.join(self.config_dir, 'app_config.json')
|
||||
self.resources_path = os.path.join(self.config_dir, 'resources')
|
||||
self.heartbeat_file = os.path.join(DATA_DIR, '.player_heartbeat')
|
||||
# Ensure all required folders exist locally
|
||||
for directory in [
|
||||
self.config_dir,
|
||||
self.resources_path,
|
||||
os.path.join(self.media_dir, 'edited_media'),
|
||||
self.playlists_dir,
|
||||
os.path.join(DATA_DIR, 'logs'),
|
||||
os.path.join(DATA_DIR, 'config', 'certs'),
|
||||
]:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
patched_main.SignagePlayer.__init__ = _patched_init
|
||||
|
||||
# Patch SSLManager cert directory to use local data folder
|
||||
# ssl_utils is imported by player_auth.py and get_playlists_v2.py, not main.py
|
||||
import ssl_utils
|
||||
ssl_utils.SSLManager.CERT_DIR = os.path.join(DATA_DIR, 'config', 'certs')
|
||||
ssl_utils.SSLManager.CERT_FILE = os.path.join(
|
||||
ssl_utils.SSLManager.CERT_DIR, 'server_cert.pem'
|
||||
)
|
||||
ssl_utils.SSLManager.CERT_INFO_FILE = os.path.join(
|
||||
ssl_utils.SSLManager.CERT_DIR, 'cert_info.json'
|
||||
)
|
||||
|
||||
# Run the app
|
||||
try:
|
||||
app = patched_main.SignagePlayerApp()
|
||||
app.run()
|
||||
except KeyboardInterrupt:
|
||||
Logger.info("Application stopped by user (Ctrl+C)")
|
||||
except SystemExit as _se:
|
||||
Logger.critical(f"Kivy SystemExit (likely window provider missing): {_se}")
|
||||
try:
|
||||
crash_log = os.path.join(DATA_DIR, 'logs', 'crash.log')
|
||||
with open(crash_log, 'w') as f:
|
||||
f.write(f"Kivy SystemExit: {_se}\n")
|
||||
f.write("This usually means Kivy could not find a window provider on this system.\n")
|
||||
except Exception:
|
||||
pass
|
||||
_show_error_box(
|
||||
"Kiwy Signage Player - Kivy Error",
|
||||
f"Kivy exited: {_se}\n\n"
|
||||
"This usually means Kivy could not create a window.\n"
|
||||
"Check your GPU drivers and DirectX installation.\n\n"
|
||||
"See logs/crash.log for details."
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
Logger.critical(f"Fatal error: {e}")
|
||||
Logger.exception("Full traceback:")
|
||||
# Also write to a crash log next to the executable
|
||||
try:
|
||||
import traceback
|
||||
crash_log = os.path.join(DATA_DIR, 'logs', 'crash.log')
|
||||
with open(crash_log, 'w') as f:
|
||||
f.write(f"Fatal error: {e}\n")
|
||||
traceback.print_exc(file=f)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(1)
|
||||
finally:
|
||||
Logger.info("Application shutdown complete")
|
||||
except BaseException as _top_e:
|
||||
# Catch any error BEFORE Logger is available (including SystemExit)
|
||||
import traceback as _tb
|
||||
_trace = _tb.format_exc()
|
||||
try:
|
||||
_crash_log = os.path.join(DATA_DIR, 'logs', 'fatal_crash.log')
|
||||
with open(_crash_log, 'w') as _f:
|
||||
_f.write(f"FATAL (pre-Logger): {_top_e}\n")
|
||||
_f.write(_trace)
|
||||
except Exception:
|
||||
pass
|
||||
_show_error_box(
|
||||
"Kiwy Signage Player - Startup Error",
|
||||
f"{_top_e}\n\nSee logs/fatal_crash.log for details."
|
||||
)
|
||||
raise # Re-raise so .exe still shows the error
|
||||
Reference in New Issue
Block a user