""" 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 # ── Try to import the embedded CEF browser ────────────────────────── _CEF_BROWSER = None def _get_cef_browser(): """Return the shared CefBrowser singleton, or None if unavailable.""" global _CEF_BROWSER if _CEF_BROWSER is None: try: from cef_browser import CefBrowser, CEF_AVAILABLE if CEF_AVAILABLE: _CEF_BROWSER = CefBrowser() else: return None except Exception: return None return _CEF_BROWSER 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 # ── Win32 API helpers via ctypes ───────────────────────────────────── class _Win32Overlay: """Fullscreen black overlay window to mask desktop during transitions. When switching away from Chromium, the browser window disappears and there is a brief moment where the desktop is visible before Kivy manages to bring its window to the front. This overlay covers that flash with a pure-black borderless always-on-top Win32 window. """ _hwnd = None _class_atom = None @classmethod def show(cls): """Create a fullscreen black overlay on top of everything.""" if cls._hwnd is not None: return # already showing try: user32 = ctypes.windll.user32 kernel32 = ctypes.windll.kernel32 # Register a simple window class WNDPROC = ctypes.WINFUNCTYPE( ctypes.c_int64, ctypes.c_int64, ctypes.c_uint, ctypes.c_uint64, ctypes.c_int64 ) @WNDPROC def wnd_proc(hwnd, msg, wparam, lparam): if msg == 0x0002: # WM_DESTROY user32.PostQuitMessage(0) if msg == 0x0014: # WM_ERASEBKGND return 1 # tell Windows we erased it return user32.DefWindowProcW(hwnd, msg, wparam, lparam) hinstance = kernel32.GetModuleHandleW(None) # Register class class_name = 'KiwyOverlay_' + str(ctypes.c_uint64(int(kernel32.GetTickCount64())).value) wc = ctypes.create_unicode_buffer(256) _WNDCLASS = ctypes.c_byte * (6 * 8) # rough size buf = _WNDCLASS() # Simple approach: use RegisterClassExW user32.RegisterClassExW.restype = ctypes.c_uint16 user32.RegisterClassExW.argtypes = [ctypes.c_void_p] # We'll use a simpler method: just create a MessageBox-style window # Actually, let's use the simplest possible approach: # Get screen dimensions screen_w = user32.GetSystemMetrics(0) # SM_CXSCREEN screen_h = user32.GetSystemMetrics(1) # SM_CYSCREEN # Create a borderless always-on-top window cls._hwnd = user32.CreateWindowExW( 0x00000008, # WS_EX_TOPMOST | WS_EX_TOOLWINDOW b'#32770', # Dialog class - always available b'', # no title 0x80000000 | 0x10000000, # WS_POPUP | WS_VISIBLE 0, 0, screen_w, screen_h, 0, 0, hinstance, 0 ) if cls._hwnd: # Make it black from ctypes import wintypes gdi32 = ctypes.windll.gdi32 hdc = user32.GetDC(cls._hwnd) rect = (ctypes.c_long * 4)(0, 0, screen_w, screen_h) brush = gdi32.CreateSolidBrush(0x00000000) # black brush gdi32.FillRect(hdc, ctypes.byref(rect), brush) gdi32.DeleteObject(brush) user32.ReleaseDC(cls._hwnd, hdc) # Force it to the top user32.SetWindowPos(cls._hwnd, -1, 0, 0, screen_w, screen_h, 0x0002 | 0x0040) user32.ShowWindow(cls._hwnd, 1) # SW_SHOWNORMAL user32.UpdateWindow(cls._hwnd) except Exception: cls._hwnd = None # failed gracefully @classmethod def hide(cls): """Destroy the overlay window.""" if cls._hwnd is None: return try: user32 = ctypes.windll.user32 user32.DestroyWindow(cls._hwnd) except Exception: pass cls._hwnd = None def _bring_kivy_to_front(): """Bring the Kivy/SDL window to foreground using win32gui. Unlike Window.raise_window(), win32gui.SetForegroundWindow actually works reliably on Windows — it uses the same Win32 API that the Task Manager uses. """ try: import win32gui import win32con def _enum_cb(hwnd, hwnd_list): cls = win32gui.GetClassName(hwnd) title = win32gui.GetWindowText(hwnd) if cls == "SDL_app": hwnd_list.append(hwnd) elif "Kiwy" in title or "Signage" in title: hwnd_list.append(hwnd) hwnd_list = [] win32gui.EnumWindows(_enum_cb, hwnd_list) if hwnd_list: kivy_hwnd = hwnd_list[-1] # most recent win32gui.ShowWindow(kivy_hwnd, win32con.SW_SHOWNORMAL) win32gui.SetForegroundWindow(kivy_hwnd) win32gui.BringWindowToTop(kivy_hwnd) except Exception: # Fallback to Kivy's built-in raise try: from kivy.core.window import Window Window.show() Window.raise_window() except Exception: pass def _windows_kill_process_tree(proc): """Kill a process AND all its children using taskkill. Chrome/Edge spawns many child processes (GPU, renderer, network, etc.). A simple proc.terminate() leaves children running, causing lingering browser windows or zombie processes. """ if proc is None or proc.poll() is not None: return try: subprocess.run( ['taskkill', '/F', '/T', '/PID', str(proc.pid)], capture_output=True, timeout=5 ) except Exception: # Fallback: try terminate + kill try: proc.terminate() try: proc.wait(timeout=3) except Exception: proc.kill() except Exception: pass 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. Strategy (tried in order): 1. CEF embedded browser (best — no subprocess, no z-order fights) 2. Chrome/Edge subprocess (fallback) """ from kivy.logger import Logger from kivy.clock import Clock from urllib.parse import urlparse scheme = urlparse(url).scheme.lower() if scheme not in ('http', 'https'): Logger.warning(f"SignagePlayer: Refusing non-http(s) weblink: {url}") self.consecutive_errors += 1 self._skip_to_next_media() return False # ── Strategy 1: CEF embedded browser ──────────────────────── cef_browser = _get_cef_browser() if cef_browser is not None: Logger.info(f"SignagePlayer: Opening weblink via CEF embedded browser: {url}") try: self.ids.content_area.opacity = 0 except Exception: pass cef_browser.show(url) Clock.unschedule(self.next_media) self._start_inactivity_watchdog(duration) self.preload_next_media() Logger.info("SignagePlayer: CEF browser launched successfully") return True # ── Strategy 2: Subprocess Chrome/Edge (fallback) ──────────── browser = _windows_find_browser() if not browser: Logger.error( "SignagePlayer: No embedded CEF and no Chrome/Edge found. " "Cannot display weblink on Windows." ) self.consecutive_errors += 1 self._skip_to_next_media() return False target_width, target_height = self._get_browser_target_size() try: Logger.info(f"SignagePlayer: Opening weblink via subprocess: {url} ({browser})") self._kill_weblink_preload() # Hide Kivy content from kivy.core.window import Window as KivyWindow try: self.ids.content_area.opacity = 0 KivyWindow.minimize() except Exception: pass _Win32Overlay.show() self._weblink_proc = subprocess.Popen([ browser, '--new-window', '--start-maximized', '--start-fullscreen', '--app=' + url, '--no-first-run', '--noerrdialogs', '--disable-infobars', '--incognito', '--no-default-browser-check', '--disable-session-crashed-bubble', '--disable-features=TranslateUI', '--disable-sync', '--disable-background-networking', '--window-position=0,0', f'--window-size={target_width},{target_height}', '--force-device-scale-factor=1', url, ], shell=False) def _hide_overlay(dt): _Win32Overlay.hide() try: KivyWindow.raise_window() except Exception: pass Clock.schedule_once(_hide_overlay, 1.5) 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}") _Win32Overlay.hide() 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 kill_weblink_after_frame for both CEF and subprocess ── def _windows_kill_weblink_after_frame(self): """Close the weblink (CEF or subprocess) immediately before next media.""" import time from kivy.logger import Logger from kivy.clock import Clock self._stop_inactivity_watchdog() self._kill_weblink_preload() # Try CEF first cef_browser = _get_cef_browser() if cef_browser is not None and cef_browser.is_showing(): Logger.info("SignagePlayer: Hiding CEF embedded browser") cef_browser.hide() self._weblink_proc = None return # Fallback: subprocess Chrome proc = self._weblink_proc self._weblink_proc = None if proc is None or proc.poll() is not None: return _Win32Overlay.show() Logger.info("SignagePlayer: Killing Chromium subprocess immediately") _windows_kill_process_tree(proc) time.sleep(0.1) _bring_kivy_to_front() _Win32Overlay.hide() signage_main.SignagePlayer._kill_weblink_after_frame = _windows_kill_weblink_after_frame # ── Patch play_current_media — same immediate-kill logic ──────── _original_play_current = signage_main.SignagePlayer.play_current_media def _windows_play_current_media(self, force_reload=False, _after_weblink=False): """Wrapped play_current_media — closes weblink immediately on transition.""" if not _after_weblink: # Kill CEF browser if showing cef_browser = _get_cef_browser() if cef_browser is not None and cef_browser.is_showing(): cef_browser.hide() self._weblink_proc = None # Kill subprocess Chrome if running proc = self._weblink_proc if proc is not None and proc.poll() is None: _Win32Overlay.show() _windows_kill_process_tree(proc) self._weblink_proc = None self._stop_inactivity_watchdog() self._kill_weblink_preload() _bring_kivy_to_front() _Win32Overlay.hide() return _original_play_current(self, force_reload=force_reload, _after_weblink=_after_weblink) signage_main.SignagePlayer.play_current_media = _windows_play_current_media # Patch _prewarm_weblink for Windows — disabled for now. # The off-screen Chrome window on Windows can interfere with: # - Audio playback (Chrome claims audio device) # - GPU resources (Chrome's GPU process runs in background) # - Taskbar icons showing duplicate Chrome windows # Pre-warming is less critical on desktop where launch is already fast. def _windows_prewarm_weblink(self, url): pass # Disabled on Windows — desktop launch is fast enough 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