Files
Kiwy-Signage/windows/run_win.py
T
ske087 6abde5a767 Fix Windows weblink handling: fullscreen Chrome, black overlay masking, proper process tree kill
Windows-specific fixes:
- _windows_play_weblink: uses --start-maximized + --app=URL for true fullscreen
- Shows black Win32 overlay before opening/closing Chrome to mask desktop
- _windows_kill_process_tree: uses taskkill /F /T to kill all Chrome child processes
- _Win32Overlay class: fullscreen borderless always-on-top black window
- Updated README to note Python 3.12 requirement and local data dir behavior
2026-07-24 13:48:49 +03:00

765 lines
30 KiB
Python

"""
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
# ── 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 _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 using Chrome/Edge.
Fixes on Windows:
- Uses --start-maximized + --window-size to ensure fullscreen
- Uses --app=URL instead of bare URL for app-like fullscreen
- Kills the entire Chrome process tree to prevent lingering
- Shows a black overlay BEFORE closing Chrome to mask desktop
"""
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
# ── Resolve target size ──────────────────────────────────────
target_width, target_height = self._get_browser_target_size()
try:
Logger.info(f"SignagePlayer: Opening weblink: {url} (browser: {browser})")
Logger.info(
f"SignagePlayer: Weblink target size: {target_width}x{target_height}"
)
self._kill_weblink_preload()
# Hide Kivy content so it doesn't show underneath Chrome
from kivy.core.window import Window as KivyWindow
try:
self.ids.content_area.opacity = 0
KivyWindow.minimize()
except Exception:
pass
# Show a black overlay BEFORE Chrome opens — masks any
# desktop flash during the transition.
_Win32Overlay.show()
# Launch in fullscreen / app mode on Windows
# --start-maximized ensures it fills the screen on first paint
# --app=URL gives a window without address bar
# --window-size ensures the browser targets the correct resolution
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',
'--no-default-browser-check',
'--window-position=0,0',
f'--window-size={target_width},{target_height}',
'--force-device-scale-factor=1',
url,
], shell=False)
# Give Chrome a moment to cover the screen, then hide overlay
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 the _kill_weblink_after_frame to work without Linux-specific code
def _windows_kill_weblink_after_frame(self):
"""Gracefully transition away from a weblink item on Windows.
Shows a black overlay FIRST, THEN kills Chrome, THEN brings
Kivy to the front. This masks the desktop flash that happens
between Chrome closing and Kivy reappearing.
"""
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 not None:
_Win32Overlay.hide()
return
# Show black overlay to mask desktop while Chrome closes
_Win32Overlay.show()
if proc is None:
_Win32Overlay.hide()
return
def _do_kill(dt):
if proc.poll() is None:
_windows_kill_process_tree(proc)
Logger.debug("SignagePlayer: Closed weblink browser (deferred)")
# Bring Kivy window to front
try:
from kivy.core.window import Window as KivyWindow
KivyWindow.show()
KivyWindow.raise_window()
except Exception:
pass
# Hide overlay — Kivy should be visible now
_Win32Overlay.hide()
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