Compare commits

...

1 Commits

Author SHA1 Message Date
skymike03
059d3988ac v2.5.0.2 (2026.02.06)
- add "versionclean" script to clean batocera version after activating custom rgsx service at boot . Thanks to the BUA project for the script.
- add encoding UTF-8 for retrobat launcher to avoid controller with non ASCII characters to crash rgsx (issue #43) thanks to Crover81
- add new menu in Menu>Settings to test connection to all usefull urls and logging
- some log trim
2026-02-06 16:26:40 +01:00
15 changed files with 620 additions and 21 deletions

View File

@@ -709,6 +709,7 @@ async def main():
"pause_games_menu",
"pause_settings_menu",
"pause_api_keys_status",
"pause_connection_status",
"filter_platforms",
"display_menu",
"language_select",
@@ -1149,6 +1150,9 @@ async def main():
elif config.menu_state == "pause_api_keys_status":
from display import draw_pause_api_keys_status
draw_pause_api_keys_status(screen)
elif config.menu_state == "pause_connection_status":
from display import draw_pause_connection_status
draw_pause_connection_status(screen)
elif config.menu_state == "filter_platforms":
from display import draw_filter_platforms_menu
draw_filter_platforms_menu(screen)

View File

@@ -0,0 +1,69 @@
#!/bin/bash
# BATOCERA SERVICE
# name: Version Clean Service
# description: Clean batocera-version output (hide extra services)
# author: batocera-unofficial-addons
# depends:
# version: 1.0
SERVICE_NAME="versionclean"
TARGET="/usr/bin/batocera-version"
BACKUP="${TARGET}.bak"
case "$1" in
start)
# If we've already backed up, assume it's already "cleaned"
if [ -f "$BACKUP" ]; then
echo "${SERVICE_NAME}: already started (backup exists at ${BACKUP})."
exit 0
fi
echo "${SERVICE_NAME}: backing up original ${TARGET} to ${BACKUP}..."
cp "$TARGET" "$BACKUP"
echo "${SERVICE_NAME}: writing clean version script to ${TARGET}..."
cat << 'EOF' > "$TARGET"
#!/bin/bash
# Clean batocera-version
# - "batocera-version --extra" -> "none"
# - "batocera-version" -> contents of /usr/share/batocera/batocera.version
if test "$1" = "--extra"; then
echo "none"
exit 0
fi
cat /usr/share/batocera/batocera.version
EOF
chmod +x "$TARGET"
echo "${SERVICE_NAME}: clean version applied."
;;
stop)
if [ -f "$BACKUP" ]; then
echo "${SERVICE_NAME}: restoring original ${TARGET} from ${BACKUP}..."
cp "$BACKUP" "$TARGET"
rm "$BACKUP"
echo "${SERVICE_NAME}: restore complete."
else
echo "${SERVICE_NAME}: no backup found, nothing to restore."
fi
;;
status)
if [ -f "$BACKUP" ]; then
echo "${SERVICE_NAME}: CLEAN VERSION ACTIVE (backup present)."
else
echo "${SERVICE_NAME}: ORIGINAL VERSION ACTIVE."
fi
;;
*)
echo "Usage: $0 {start|stop|status}"
exit 1
;;
esac
exit 0

View File

@@ -14,7 +14,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.5.0.1"
app_version = "2.5.0.2"
# Nombre de jours avant de proposer la mise à jour de la liste des jeux
GAMELIST_UPDATE_DAYS = 7
@@ -141,6 +141,12 @@ pending_download_is_queue = False # Indique si pending_download doit être ajou
# Indique si un téléchargement est en cours
download_active = False
# Cache status de connexion (menu pause > settings)
connection_status = {}
connection_status_timestamp = 0.0
connection_status_in_progress = False
connection_status_progress = {"done": 0, "total": 0}
# Log directory
# Docker mode: /config/logs (persisted in config volume)
# Traditional mode: /app/RGSX/logs (current behavior)
@@ -347,6 +353,7 @@ platforms = [] # Liste des plateformes disponibles
current_platform = 0 # Index de la plateforme actuelle sélectionnée
platform_names = {} # {platform_id: platform_name}
games_count = {} # Dictionnaire comptant le nombre de jeux par plateforme
games_count_log_verbose = False # Log détaillé par fichier (sinon résumé compact)
platform_dicts = [] # Liste des dictionnaires de plateformes
# Filtre plateformes

View File

@@ -17,7 +17,8 @@ from utils import (
save_music_config, load_api_keys, _get_dest_folder_name,
extract_zip, extract_rar, find_file_with_or_without_extension, toggle_web_service_at_boot, check_web_service_status,
restart_application, generate_support_zip, load_sources,
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string
ensure_download_provider_keys, missing_all_provider_keys, build_provider_paths_string,
start_connection_status_check
)
from history import load_history, clear_history, add_to_history, save_history
from language import _, get_available_languages, set_language
@@ -54,6 +55,7 @@ VALID_STATES = [
"pause_games_menu", # sous-menu Games (source mode, update/redownload cache)
"pause_settings_menu", # sous-menu Settings (music on/off, symlink toggle, api keys status)
"pause_api_keys_status", # sous-menu API Keys (affichage statut des clés)
"pause_connection_status", # sous-menu Connection status (statut accès sites)
# Nouveaux menus historique
"history_game_options", # menu options pour un jeu de l'historique
"history_show_folder", # afficher le dossier de téléchargement
@@ -74,6 +76,8 @@ VALID_STATES = [
]
def validate_menu_state(state):
if not state:
return "platform"
if state not in VALID_STATES:
logger.debug(f"État invalide {state}, retour à platform")
return "platform"
@@ -2074,21 +2078,23 @@ def handle_controls(event, sources, joystick, screen):
elif config.menu_state == "pause_settings_menu":
sel = getattr(config, 'pause_settings_selection', 0)
# Calculer le nombre total d'options selon le système
# Liste des options : music, symlink, auto_extract, roms_folder, [web_service], [custom_dns], api keys, back
total = 6 # music, symlink, auto_extract, roms_folder, api keys, back (Windows)
# Liste des options : music, symlink, auto_extract, roms_folder, [web_service], [custom_dns], api keys, connection_status, back
total = 7 # music, symlink, auto_extract, roms_folder, api keys, connection_status, back (Windows)
auto_extract_index = 2
roms_folder_index = 3
web_service_index = -1
custom_dns_index = -1
api_keys_index = 4
back_index = 5
connection_status_index = 5
back_index = 6
if config.OPERATING_SYSTEM == "Linux":
total = 8 # music, symlink, auto_extract, roms_folder, web_service, custom_dns, api keys, back
total = 9 # music, symlink, auto_extract, roms_folder, web_service, custom_dns, api keys, connection_status, back
web_service_index = 4
custom_dns_index = 5
api_keys_index = 6
back_index = 7
connection_status_index = 7
back_index = 8
if is_input_matched(event, "up"):
config.pause_settings_selection = (sel - 1) % total
@@ -2208,6 +2214,11 @@ def handle_controls(event, sources, joystick, screen):
elif sel == api_keys_index and is_input_matched(event, "confirm"):
config.menu_state = "pause_api_keys_status"
config.needs_redraw = True
# Option Connection Status
elif sel == connection_status_index and is_input_matched(event, "confirm"):
start_connection_status_check(force=True)
config.menu_state = "pause_connection_status"
config.needs_redraw = True
# Option Back (dernière option)
elif sel == back_index and is_input_matched(event, "confirm"):
config.menu_state = "pause_menu"
@@ -2224,6 +2235,12 @@ def handle_controls(event, sources, joystick, screen):
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
elif config.menu_state == "pause_connection_status":
if is_input_matched(event, "cancel") or is_input_matched(event, "confirm") or is_input_matched(event, "start"):
config.menu_state = "pause_settings_menu"
config.last_state_change_time = pygame.time.get_ticks()
config.needs_redraw = True
# Aide contrôles
elif config.menu_state == "controls_help":
if is_input_matched(event, "cancel"):

View File

@@ -5,10 +5,12 @@ import os
import io
import platform
import random
from datetime import datetime
import config
from utils import (truncate_text_middle, wrap_text, load_system_image, truncate_text_end,
check_web_service_status, check_custom_dns_status, load_api_keys,
_get_dest_folder_name, find_file_with_or_without_extension)
_get_dest_folder_name, find_file_with_or_without_extension,
get_connection_status_targets, get_connection_status_snapshot)
import logging
import math
from history import load_history, is_game_downloaded
@@ -1965,6 +1967,9 @@ def draw_controls(screen, menu_state, current_music_name=None, music_popup_start
("clear_history", _("settings_roms_folder_default")),
("cancel", _("controls_cancel_back")),
],
"pause_connection_status": [
("cancel", _("controls_cancel_back")),
],
}
# Cas spécial : pause_settings_menu avec option roms_folder sélectionnée
@@ -2948,6 +2953,7 @@ def draw_pause_settings_menu(screen, selected_index):
custom_dns_txt = f"{_('settings_custom_dns')} : < {custom_dns_status} >"
api_keys_txt = _("menu_api_keys_status") if _ else "API Keys"
connection_status_txt = _("menu_connection_status") if _ else "Connection status"
back_txt = _("menu_back") if _ else "Back"
# Construction de la liste des options
@@ -2956,7 +2962,7 @@ def draw_pause_settings_menu(screen, selected_index):
options.append(web_service_txt)
if custom_dns_txt: # Ajouter seulement si Linux/Batocera
options.append(custom_dns_txt)
options.extend([api_keys_txt, back_txt])
options.extend([api_keys_txt, connection_status_txt, back_txt])
# Index de l'option Dossier ROMs
roms_folder_index = 3
@@ -2974,6 +2980,7 @@ def draw_pause_settings_menu(screen, selected_index):
instruction_keys.append("instruction_settings_custom_dns")
instruction_keys.extend([
"instruction_settings_api_keys",
"instruction_settings_connection_status",
"instruction_generic_back",
])
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
@@ -3084,6 +3091,147 @@ def draw_pause_api_keys_status(screen):
screen.blit(hint_surf, hint_rect)
def draw_pause_connection_status(screen):
screen.blit(OVERLAY, (0, 0))
status_map, last_ts, in_progress, progress = get_connection_status_snapshot()
targets = get_connection_status_targets()
title = _("connection_status_title") if _ else "Connection status"
cat_updates = _("connection_status_category_updates") if _ else "Updates"
cat_sources = _("connection_status_category_sources") if _ else "Sources"
# Group rows by category
categories_order = ["updates", "sources"]
category_labels = {
"updates": cat_updates,
"sources": cat_sources,
}
rows = [] # list of (type, data)
for cat in categories_order:
cat_items = [t for t in targets if t.get("category") == cat]
if not cat_items:
continue
rows.append(("header", category_labels.get(cat, cat)))
for item in cat_items:
rows.append(("item", item))
# Title surface (used for sizing)
title_surface = config.font.render(title, True, THEME_COLORS["text"])
# Dimensions
row_height = config.small_font.get_height() + 14
header_row_height = config.small_font.get_height() + 10
title_height = 60
footer_height = 55
content_height = 0
for row_type, row_data in rows:
content_height += header_row_height if row_type == "header" else row_height
# Measure max text width to size the menu
max_text_width = title_surface.get_width()
for row_type, row_data in rows:
if row_type == "header":
w = config.small_font.size(str(row_data))[0]
else:
label = row_data.get("label") or row_data.get("key", "")
w = config.small_font.size(str(label))[0]
if w > max_text_width:
max_text_width = w
circle_area_width = 46 # status circle + gap
inner_padding = 70
menu_width = min(int(config.screen_width * 0.70), max(360, max_text_width + circle_area_width + inner_padding))
menu_height = title_height + content_height + footer_height
menu_x = (config.screen_width - menu_width) // 2
menu_y = (config.screen_height - menu_height) // 2
pygame.draw.rect(screen, THEME_COLORS["button_idle"], (menu_x, menu_y, menu_width, menu_height), border_radius=22)
pygame.draw.rect(screen, THEME_COLORS["border"], (menu_x, menu_y, menu_width, menu_height), 2, border_radius=22)
# Title
title_rect = title_surface.get_rect(center=(config.screen_width // 2, menu_y + 34))
screen.blit(title_surface, title_rect)
# Columns
col_site_x = menu_x + 40
col_status_x = menu_x + int(menu_width * 0.70)
y = menu_y + title_height - 5
for row_type, data in rows:
if row_type == "header":
header_text = data
header_surf = config.small_font.render(header_text, True, THEME_COLORS.get("text_dim", THEME_COLORS["text"]))
screen.blit(header_surf, (col_site_x, y))
# separator line
sep_y = y + header_row_height - 6
pygame.draw.line(screen, THEME_COLORS["border"], (menu_x + 25, sep_y), (menu_x + menu_width - 25, sep_y), 1)
y += header_row_height
continue
item = data
key = item.get("key")
label = item.get("label") or item.get("key", "")
status_val = status_map.get(key)
if status_val is True:
circle_color = (60, 170, 60)
circle_bg = (30, 70, 30)
elif status_val is False:
circle_color = (180, 55, 55)
circle_bg = (70, 25, 25)
else:
circle_color = (140, 140, 140)
circle_bg = (60, 60, 60)
# Site label (indent to distinguish from category title)
label_surf = config.small_font.render(label, True, THEME_COLORS["text"])
screen.blit(label_surf, (col_site_x + 18, y))
# Status circle
radius = 14
center_x = col_status_x + radius
center_y = y + config.small_font.get_height() // 2
pygame.draw.circle(screen, circle_bg, (center_x, center_y), radius)
pygame.draw.circle(screen, circle_color, (center_x, center_y), radius, 2)
# Separator
sep_y = y + row_height - 8
pygame.draw.line(screen, THEME_COLORS["border"], (menu_x + 25, sep_y), (menu_x + menu_width - 25, sep_y), 1)
y += row_height
# Footer hint
hint_font = config.tiny_font if hasattr(config, "tiny_font") else config.small_font
if in_progress:
done = int(progress.get("done", 0)) if isinstance(progress, dict) else 0
total = int(progress.get("total", 0)) if isinstance(progress, dict) else 0
if _ and _("connection_status_progress") != "connection_status_progress":
try:
hint_txt = _("connection_status_progress").format(done=done, total=total)
except Exception:
hint_txt = _("connection_status_checking") if _ else "Checking..."
else:
hint_txt = f"Checking... {done}/{total}" if total else ("Checking..." if not _ else _("connection_status_checking"))
elif last_ts:
try:
time_str = datetime.fromtimestamp(last_ts).strftime("%H:%M:%S")
except Exception:
time_str = ""
if _ and _("connection_status_last_check") != "connection_status_last_check":
try:
hint_txt = _("connection_status_last_check").format(time=time_str)
except Exception:
hint_txt = f"Last check: {time_str}" if time_str else ""
else:
hint_txt = f"Last check: {time_str}" if time_str else ""
else:
hint_txt = ""
if hint_txt:
hint_surf = hint_font.render(hint_txt, True, THEME_COLORS.get("text_dim", THEME_COLORS["text"]))
hint_rect = hint_surf.get_rect(center=(config.screen_width // 2, menu_y + menu_height - 26))
screen.blit(hint_surf, hint_rect)
def draw_filter_platforms_menu(screen):
"""Affiche le menu de filtrage des plateformes (afficher/masquer)."""
screen.blit(OVERLAY, (0, 0))

View File

@@ -189,7 +189,14 @@
"status_present": "Vorhanden",
"status_missing": "Fehlt",
"menu_api_keys_status": "API-Schlüssel",
"menu_connection_status": "Verbindungsstatus",
"api_keys_status_title": "Status der API-Schlüssel",
"connection_status_title": "Verbindungsstatus",
"connection_status_category_updates": "App-/Gamelist-Update",
"connection_status_category_sources": "Spielquellen",
"connection_status_checking": "Prüfe...",
"connection_status_progress": "Prüfe... {done}/{total}",
"connection_status_last_check": "Letzte Prüfung: {time}",
"menu_games": "Spiele",
"api_keys_hint_manage": "Legen Sie Ihre Schlüssel in {path}",
"api_key_empty_suffix": "leer",
@@ -230,6 +237,7 @@
"instruction_settings_auto_extract": "Automatische Archivextraktion nach Download aktivieren/deaktivieren",
"instruction_settings_roms_folder": "Standard-Download-Verzeichnis für ROMs ändern",
"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen",
"instruction_settings_connection_status": "Zugriff auf Update- und Quellen-Seiten prüfen",
"instruction_settings_web_service": "Web-Dienst Autostart beim Booten aktivieren/deaktivieren",
"instruction_settings_custom_dns": "Custom DNS (Cloudflare 1.1.1.1) beim Booten aktivieren/deaktivieren",
"settings_auto_extract": "Auto-Extraktion Archive",

View File

@@ -188,7 +188,14 @@
"status_present": "Present",
"status_missing": "Missing",
"menu_api_keys_status": "API Keys",
"menu_connection_status": "Connection status",
"api_keys_status_title": "API Keys Status",
"connection_status_title": "Connection status",
"connection_status_category_updates": "App/Gamelist update",
"connection_status_category_sources": "Game sources",
"connection_status_checking": "Checking...",
"connection_status_progress": "Checking... {done}/{total}",
"connection_status_last_check": "Last check: {time}",
"menu_games": "Games",
"api_keys_hint_manage": "Put your keys in {path}",
"api_key_empty_suffix": "empty",
@@ -232,6 +239,7 @@
"instruction_settings_auto_extract": "Toggle automatic archive extraction after download",
"instruction_settings_roms_folder": "Change the default ROMs download directory",
"instruction_settings_api_keys": "See detected premium provider API keys",
"instruction_settings_connection_status": "Check access to update and source sites",
"instruction_settings_web_service": "Enable/disable web service auto-start at boot",
"instruction_settings_custom_dns": "Enable/disable custom DNS (Cloudflare 1.1.1.1) at boot",
"settings_auto_extract": "Auto Extract Archives",

View File

@@ -189,7 +189,14 @@
"status_present": "Presente",
"status_missing": "Ausente",
"menu_api_keys_status": "Claves API",
"menu_connection_status": "Estado de conexión",
"api_keys_status_title": "Estado de las claves API",
"connection_status_title": "Estado de conexión",
"connection_status_category_updates": "Actualización app/lista de juegos",
"connection_status_category_sources": "Fuentes de juegos",
"connection_status_checking": "Comprobando...",
"connection_status_progress": "Comprobando... {done}/{total}",
"connection_status_last_check": "Última comprobación: {time}",
"menu_games": "Juegos",
"api_keys_hint_manage": "Coloca tus claves en {path}",
"api_key_empty_suffix": "vacío",
@@ -230,6 +237,7 @@
"instruction_settings_auto_extract": "Activar/desactivar extracción automática de archivos después de descargar",
"instruction_settings_roms_folder": "Cambiar el directorio de descarga de ROMs por defecto",
"instruction_settings_api_keys": "Ver claves API premium detectadas",
"instruction_settings_connection_status": "Comprobar acceso a sitios de actualizaciones y fuentes",
"instruction_settings_web_service": "Activar/desactivar inicio automático del servicio web",
"instruction_settings_custom_dns": "Activar/desactivar DNS personalizado (Cloudflare 1.1.1.1) al inicio",
"settings_auto_extract": "Extracción auto de archivos",

View File

@@ -185,7 +185,14 @@
"status_present": "Présente",
"status_missing": "Absente",
"menu_api_keys_status": "Clés API",
"menu_connection_status": "État de connexion",
"api_keys_status_title": "Statut des clés API",
"connection_status_title": "État de connexion",
"connection_status_category_updates": "Mise à jour App/Liste de jeux",
"connection_status_category_sources": "Sources de jeux",
"connection_status_checking": "Vérification...",
"connection_status_progress": "Vérification... {done}/{total}",
"connection_status_last_check": "Dernière vérif : {time}",
"menu_games": "Jeux",
"api_keys_hint_manage": "Placez vos clés dans {path}",
"api_key_empty_suffix": "vide",
@@ -232,6 +239,7 @@
"instruction_settings_auto_extract": "Activer/désactiver l'extraction automatique des archives après téléchargement",
"instruction_settings_roms_folder": "Changer le répertoire de téléchargement des ROMs par défaut",
"instruction_settings_api_keys": "Voir les clés API détectées des services premium",
"instruction_settings_connection_status": "Vérifier l'accès aux sites d'update et de sources",
"instruction_settings_web_service": "Activer/désactiver le démarrage automatique du service web",
"instruction_settings_custom_dns": "Activer/désactiver les DNS personnalisés (Cloudflare 1.1.1.1) au démarrage",
"settings_auto_extract": "Extraction auto des archives",

View File

@@ -184,7 +184,14 @@
"status_present": "Presente",
"status_missing": "Assente",
"menu_api_keys_status": "Chiavi API",
"menu_connection_status": "Stato connessione",
"api_keys_status_title": "Stato delle chiavi API",
"connection_status_title": "Stato connessione",
"connection_status_category_updates": "Aggiornamento app/lista giochi",
"connection_status_category_sources": "Sorgenti giochi",
"connection_status_checking": "Verifica in corso...",
"connection_status_progress": "Verifica in corso... {done}/{total}",
"connection_status_last_check": "Ultimo controllo: {time}",
"menu_games": "Giochi",
"api_keys_hint_manage": "Metti le tue chiavi in {path}",
"api_key_empty_suffix": "vuoto",
@@ -225,6 +232,7 @@
"instruction_settings_auto_extract": "Attivare/disattivare estrazione automatica archivi dopo il download",
"instruction_settings_roms_folder": "Cambiare la directory di download ROMs predefinita",
"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate",
"instruction_settings_connection_status": "Verifica accesso ai siti di aggiornamento e sorgenti",
"instruction_settings_web_service": "Attivare/disattivare avvio automatico servizio web all'avvio",
"instruction_settings_custom_dns": "Attivare/disattivare DNS personalizzato (Cloudflare 1.1.1.1) all'avvio",
"settings_auto_extract": "Estrazione auto archivi",

View File

@@ -190,7 +190,14 @@
"status_present": "Presente",
"status_missing": "Ausente",
"menu_api_keys_status": "Chaves API",
"menu_connection_status": "Estado da conexão",
"api_keys_status_title": "Status das chaves API",
"connection_status_title": "Estado da conexão",
"connection_status_category_updates": "Atualização do app/lista de jogos",
"connection_status_category_sources": "Fontes de jogos",
"connection_status_checking": "Verificando...",
"connection_status_progress": "Verificando... {done}/{total}",
"connection_status_last_check": "Última verificação: {time}",
"menu_games": "Jogos",
"api_keys_hint_manage": "Coloque suas chaves em {path}",
"api_key_empty_suffix": "vazio",
@@ -231,6 +238,7 @@
"instruction_settings_auto_extract": "Ativar/desativar extração automática de arquivos após download",
"instruction_settings_roms_folder": "Alterar o diretório de download de ROMs padrão",
"instruction_settings_api_keys": "Ver chaves API premium detectadas",
"instruction_settings_connection_status": "Verificar acesso a sites de atualização e fontes",
"instruction_settings_web_service": "Ativar/desativar início automático do serviço web na inicialização",
"instruction_settings_custom_dns": "Ativar/desativar DNS personalizado (Cloudflare 1.1.1.1) na inicialização",
"settings_auto_extract": "Extração auto de arquivos",

View File

@@ -2068,18 +2068,47 @@ def run_server(host='0.0.0.0', port=5000):
class ReuseAddrHTTPServer(HTTPServer):
allow_reuse_address = True
# Tuer les processus existants utilisant le port
# Tuer les processus existants utilisant le port (plateforme spécifique)
try:
import subprocess
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True, timeout=2)
pids = result.stdout.strip().split('\n')
for pid in pids:
if pid:
try:
subprocess.run(['kill', '-9', pid], timeout=2)
logger.info(f"Processus {pid} tué (port {port} libéré)")
except Exception as e:
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
# Windows: utiliser netstat + taskkill
if os.name == 'nt' or getattr(config, 'OPERATING_SYSTEM', '').lower() == 'windows':
try:
netstat = subprocess.run(['netstat', '-ano'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=3)
lines = netstat.stdout.splitlines()
pids = set()
for line in lines:
parts = line.split()
if len(parts) >= 5:
local = parts[1]
pid = parts[-1]
if local.endswith(f':{port}'):
pids.add(pid)
for pid in pids:
# Safer: ignore PID 0 and non-numeric entries (system / header lines)
if not pid or not pid.isdigit():
continue
pid_int = int(pid)
if pid_int <= 0:
continue
try:
subprocess.run(['taskkill', '/PID', pid, '/F'], timeout=3)
logger.info(f"Processus {pid} tué (port {port} libéré) [Windows]")
except Exception as e:
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
except Exception as e:
logger.debug(f"Windows port release check failed: {e}")
else:
# Unix-like: utiliser lsof + kill
result = subprocess.run(['lsof', '-ti', f':{port}'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=2)
pids = result.stdout.strip().split('\n')
for pid in pids:
if pid:
try:
subprocess.run(['kill', '-9', pid], timeout=2)
logger.info(f"Processus {pid} tué (port {port} libéré)")
except Exception as e:
logger.warning(f"Impossible de tuer le processus {pid}: {e}")
except Exception as e:
logger.warning(f"Impossible de libérer le port {port}: {e}")

View File

@@ -171,6 +171,121 @@ DO NOT share this file publicly as it may contain sensitive information.
return (False, str(e), None)
VERSIONCLEAN_SERVICE_NAME = "versionclean"
VERSIONCLEAN_BACKUP_PATH = "/usr/bin/batocera-version.bak"
def _get_enabled_services():
"""Retourne la liste des services activés dans batocera-settings, ou None si indisponible."""
try:
result = subprocess.run(
["batocera-settings-get", "system.services"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode != 0:
logger.warning(f"batocera-settings-get failed: {result.stderr}")
return None
return result.stdout.split()
except FileNotFoundError:
logger.warning("batocera-settings-get command not found")
return None
except Exception as e:
logger.warning(f"Failed to read enabled services: {e}")
return None
def _ensure_versionclean_service():
"""Installe et active versionclean si nécessaire.
- Installe uniquement si le service n'est pas déjà présent.
- Active uniquement si le service n'est pas déjà activé.
- Démarre uniquement si le nettoyage n'est pas déjà appliqué.
"""
try:
if config.OPERATING_SYSTEM != "Linux":
return (True, "Versionclean skipped (non-Linux)")
services_dir = "/userdata/system/services"
service_file = os.path.join(services_dir, VERSIONCLEAN_SERVICE_NAME)
source_file = os.path.join(config.APP_FOLDER, "assets", "progs", VERSIONCLEAN_SERVICE_NAME)
if not os.path.exists(service_file):
try:
os.makedirs(services_dir, exist_ok=True)
except Exception as e:
error_msg = f"Failed to create services directory: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
if not os.path.exists(source_file):
error_msg = f"Source service file not found: {source_file}"
logger.error(error_msg)
return (False, error_msg)
try:
shutil.copy2(source_file, service_file)
os.chmod(service_file, 0o755)
logger.info(f"Versionclean service installed: {service_file}")
except Exception as e:
error_msg = f"Failed to copy versionclean service file: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
else:
logger.debug("Versionclean service already present, skipping install")
enabled_services = _get_enabled_services()
if enabled_services is None or VERSIONCLEAN_SERVICE_NAME not in enabled_services:
try:
result = subprocess.run(
["batocera-services", "enable", VERSIONCLEAN_SERVICE_NAME],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
error_msg = f"batocera-services enable versionclean failed: {result.stderr}"
logger.error(error_msg)
return (False, error_msg)
logger.debug(f"Versionclean enabled: {result.stdout}")
except FileNotFoundError:
error_msg = "batocera-services command not found"
logger.error(error_msg)
return (False, error_msg)
except Exception as e:
error_msg = f"Failed to enable versionclean: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
else:
logger.debug("Versionclean already enabled, skipping enable")
if os.path.exists(VERSIONCLEAN_BACKUP_PATH):
logger.debug("Versionclean already active (backup present), skipping start")
return (True, "Versionclean already active")
try:
result = subprocess.run(
["batocera-services", "start", VERSIONCLEAN_SERVICE_NAME],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
logger.warning(f"batocera-services start versionclean warning: {result.stderr}")
else:
logger.debug(f"Versionclean started: {result.stdout}")
except Exception as e:
logger.warning(f"Failed to start versionclean (non-critical): {str(e)}")
return (True, "Versionclean ensured")
except Exception as e:
error_msg = f"Unexpected versionclean error: {str(e)}"
logger.exception(error_msg)
return (False, error_msg)
def toggle_web_service_at_boot(enable: bool):
"""Active ou désactive le service web au démarrage de Batocera.
@@ -203,6 +318,11 @@ def toggle_web_service_at_boot(enable: bool):
error_msg = f"Failed to create services directory: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
# 1b. Assurer versionclean (install/enable/start si nécessaire)
ensure_ok, ensure_msg = _ensure_versionclean_service()
if not ensure_ok:
return (False, ensure_msg)
# 2. Copier le fichier rgsx_web
try:
@@ -339,6 +459,11 @@ def toggle_custom_dns_at_boot(enable: bool):
error_msg = f"Failed to create services directory: {str(e)}"
logger.error(error_msg)
return (False, error_msg)
# 1b. Assurer versionclean (install/enable/start si nécessaire)
ensure_ok, ensure_msg = _ensure_versionclean_service()
if not ensure_ok:
return (False, ensure_msg)
# 2. Copier le fichier custom_dns
try:
@@ -479,6 +604,149 @@ def check_custom_dns_status():
return False
CONNECTION_STATUS_TTL_SECONDS = 120
def get_connection_status_targets():
"""Retourne la liste des sites à vérifier pour le status de connexion."""
return [
{
"key": "retrogamesets",
"label": "Retrogamesets.fr",
"url": "https://retrogamesets.fr",
"category": "updates",
},
{
"key": "github",
"label": "GitHub.com",
"url": "https://github.com",
"category": "updates",
},
{
"key": "myrient",
"label": "Myrient.erista.me",
"url": "https://myrient.erista.me",
"category": "sources",
},
{
"key": "1fichier",
"label": "1fichier.com",
"url": "https://1fichier.com",
"category": "sources",
},
{
"key": "archive",
"label": "Archive.org",
"url": "https://archive.org",
"category": "sources",
},
]
def _check_url_connectivity(url: str, timeout: int = 6) -> bool:
"""Teste rapidement la connectivité à une URL (DNS + HTTPS)."""
headers = {"User-Agent": "RGSX-Connectivity/1.0"}
try:
try:
import requests # type: ignore
try:
response = requests.head(url, timeout=timeout, allow_redirects=True, headers=headers)
if response.status_code < 500:
return True
except Exception:
pass
try:
response = requests.get(url, timeout=timeout, allow_redirects=True, stream=True, headers=headers)
return response.status_code < 500
except Exception:
return False
except Exception:
import urllib.request
try:
req = urllib.request.Request(url, method="HEAD", headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status < 500
except Exception:
try:
req = urllib.request.Request(url, method="GET", headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status < 500
except Exception:
return False
except Exception:
return False
def start_connection_status_check(force: bool = False) -> None:
"""Lance un check asynchrone des sites (avec cache/TTL)."""
try:
now = time.time()
if getattr(config, "connection_status_in_progress", False):
return
last_ts = getattr(config, "connection_status_timestamp", 0.0) or 0.0
if not force and last_ts and now - last_ts < CONNECTION_STATUS_TTL_SECONDS:
return
targets = get_connection_status_targets()
status = getattr(config, "connection_status", {})
if not isinstance(status, dict):
status = {}
if not status:
for item in targets:
status[item["key"]] = None
config.connection_status = status
config.connection_status_in_progress = True
config.connection_status_progress = {"done": 0, "total": len(targets)}
def _worker():
try:
results = {}
done = 0
total = len(targets)
for item in targets:
results[item["key"]] = _check_url_connectivity(item["url"])
done += 1
config.connection_status_progress = {"done": done, "total": total}
try:
config.needs_redraw = True
except Exception:
pass
config.connection_status.update(results)
config.connection_status_timestamp = time.time()
try:
summary = ", ".join([f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items()])
logger.info(f"Connection status results: {summary}")
except Exception:
pass
try:
config.needs_redraw = True
except Exception:
pass
except Exception as e:
logger.debug(f"Connection status check failed: {e}")
finally:
config.connection_status_in_progress = False
threading.Thread(target=_worker, daemon=True).start()
except Exception as e:
logger.debug(f"Failed to start connection status check: {e}")
def get_connection_status_snapshot():
"""Retourne (status_dict, timestamp, in_progress, progress)."""
status = getattr(config, "connection_status", {})
if not isinstance(status, dict):
status = {}
ts = getattr(config, "connection_status_timestamp", 0.0) or 0.0
in_progress = getattr(config, "connection_status_in_progress", False)
progress = getattr(config, "connection_status_progress", {"done": 0, "total": 0})
if not isinstance(progress, dict):
progress = {"done": 0, "total": 0}
return status, ts, in_progress, progress
_extensions_cache = None # type: ignore
_extensions_json_regenerated = False
@@ -886,6 +1154,12 @@ def load_sources():
for platform_name in config.platforms:
games = load_games(platform_name)
config.games_count[platform_name] = len(games)
if config.games_count:
try:
summary = ", ".join([f"{name}: {count}" for name, count in config.games_count.items()])
logger.debug(f"Nombre de jeux par système: {summary}")
except Exception:
pass
return sources
except Exception as e:
logger.error(f"Erreur fusion systèmes + détection jeux: {e}")
@@ -958,7 +1232,8 @@ def load_games(platform_id):
else:
logger.warning(f"Format de fichier jeux inattendu pour {platform_id}: {type(data)}")
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
if getattr(config, "games_count_log_verbose", False):
logger.debug(f"{os.path.basename(game_file)}: {len(normalized)} jeux")
return normalized
except Exception as e:
logger.error(f"Erreur lors du chargement des jeux pour {platform_id}: {e}")

View File

@@ -1,3 +1,3 @@
{
"version": "2.5.0.0"
"version": "2.5.0.2"
}

View File

@@ -301,6 +301,7 @@ set PYGAME_HIDE_SUPPORT_PROMPT=1
set SDL_VIDEODRIVER=windows
set SDL_AUDIODRIVER=directsound
set PYTHONWARNINGS=ignore::UserWarning:pygame.pkgdata
set PYTHONIOENCODING=utf-8
:: =============================================================================
:: Configuration multi-ecran
@@ -331,6 +332,7 @@ echo [%DATE% %TIME%] Environment variables set: >> "%LOG_FILE%"
echo [%DATE% %TIME%] RGSX_ROOT=%RGSX_ROOT% >> "%LOG_FILE%"
echo [%DATE% %TIME%] SDL_VIDEODRIVER=%SDL_VIDEODRIVER% >> "%LOG_FILE%"
echo [%DATE% %TIME%] SDL_AUDIODRIVER=%SDL_AUDIODRIVER% >> "%LOG_FILE%"
echo [%DATE% %TIME%] PYTHONIOENCODING=%PYTHONIOENCODING% >> "%LOG_FILE%"
echo.
if defined DISPLAY_NUM (