From 30c1ec082903d5cf765ca1d9b33cabd338994cbf Mon Sep 17 00:00:00 2001 From: Xalcker Date: Sat, 3 Oct 2026 12:15:19 -0700 Subject: [PATCH 1/2] chore: fuerza LF en todos los archivos de texto (eol=lf) Con core.autocrlf=true en Windows los .yml y .example se descargaban en CRLF y rompian las pruebas de codificacion y de input de RPCS3. Co-Authored-By: Claude Sonnet 5.5 --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index fafb369..ed99b2e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ # Normaliza finales de linea: los scripts y pruebas deben ser LF aunque se # clonen en Windows (un CRLF rompe el shebang y bats). -* text=auto +* text=auto eol=lf *.sh text eol=lf *.bats text eol=lf *.md text eol=lf From c44b5dee318a0e9e4efe2ad019fe18237c172373 Mon Sep 17 00:00:00 2001 From: Xalcker Date: Sat, 3 Oct 2026 12:15:19 -0700 Subject: [PATCH 2/2] feat: audio HDMI, cuantum, atajo de salida, rendimiento y updaters comunes a YARG, Clone Hero y RPCS3 Mueve a lib/kiosk_runtime.sh lo que solo tenia RPCS3: - salida de audio por HDMI (regla de WirePlumber) y volumen al arrancar - cuantum de PipeWire (*_PIPEWIRE_QUANTUM, 128 por defecto) - atajo Ctrl+Alt+Q (kiosk-exit-hotkey.py, *_EXIT_HOTKEY) - configure_kiosk_performance en lugar de tres copias - updaters que no descargan si ya esta la ultima version (--force) - disco minimo por ruta (YARG_MIN_DISK_GB, CLONEHERO_MIN_DISK_GB, 32 GB) Co-Authored-By: Claude Sonnet 5.5 --- .env.example | 22 +++ CHANGELOG.md | 9 + README.md | 26 ++- install-cage-clonehero.sh | 22 ++- install-cage-rpcs3.sh | 18 +- install-cage-yarg.sh | 22 ++- lib/cage.sh | 3 +- lib/clonehero.sh | 55 ++---- lib/kiosk_runtime.sh | 363 +++++++++++++++++++++++++++++++++- lib/rpcs3.sh | 307 +--------------------------- lib/yarg.sh | 52 ++--- tests/test_kiosk_runtime.bats | 154 +++++++++++++++ tests/test_rpcs3.bats | 35 ++-- 13 files changed, 662 insertions(+), 426 deletions(-) diff --git a/.env.example b/.env.example index d359a39..ef93897 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,17 @@ YARG_RESOLUTION=ask YARG_FORCE_SOFTWARE_RENDER=false # always muestra menu al salir de YARG; restart relanza YARG directo. YARG_EXIT_MENU=always +# Salida de audio preferida: hdmi (HDMI/DP), analog o auto. Si el dispositivo preferido +# no existe se usa el otro. Volumen de 0 a 1 al arrancar (1.0 = 100 %); vacio no lo toca. +YARG_AUDIO_OUTPUT=hdmi +YARG_AUDIO_VOLUME=1.0 +# Tamano de ciclo de PipeWire en muestras (48 kHz): 128 = 2.7 ms, 1024 = 21 ms (el +# valor por defecto de PipeWire). Menos = menos latencia, pero mas carga de CPU. Vacio no lo toca. +YARG_PIPEWIRE_QUANTUM=128 +# true instala el atajo para cerrar YARG con Ctrl+Alt+Q (teclado). +YARG_EXIT_HOTKEY=true +# Disco minimo en GB: las canciones y el /home no caben en un disco de 16 GB. +YARG_MIN_DISK_GB=32 # Clone Hero / Cage # latest consulta GitHub; url usa CLONEHERO_URL manualmente; ask pregunta al instalar. @@ -74,6 +85,17 @@ CLONEHERO_RESOLUTION=ask CLONEHERO_FORCE_SOFTWARE_RENDER=false # always muestra menu al salir de Clone Hero; restart relanza Clone Hero directo. CLONEHERO_EXIT_MENU=always +# Salida de audio preferida: hdmi (HDMI/DP), analog o auto. Si el dispositivo preferido +# no existe se usa el otro. Volumen de 0 a 1 al arrancar (1.0 = 100 %); vacio no lo toca. +CLONEHERO_AUDIO_OUTPUT=hdmi +CLONEHERO_AUDIO_VOLUME=1.0 +# Tamano de ciclo de PipeWire en muestras (48 kHz): 128 = 2.7 ms, 1024 = 21 ms (el +# valor por defecto de PipeWire). Menos = menos latencia, pero mas carga de CPU. Vacio no lo toca. +CLONEHERO_PIPEWIRE_QUANTUM=128 +# true instala el atajo para cerrar Clone Hero con Ctrl+Alt+Q (teclado). +CLONEHERO_EXIT_HOTKEY=true +# Disco minimo en GB: las canciones y el /home no caben en un disco de 16 GB. +CLONEHERO_MIN_DISK_GB=32 # RPCS3 / Cage (Rock Band 3) # RPCS3_URL vacio consulta el ultimo release de RPCS3/rpcs3-binaries-linux. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0afba87..414c86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,15 @@ Infraestructura: `test_docs` (coherencia de la documentacion con el repo), `test_assets` y `test_song_paths_and_menu`. - `.gitattributes` para mantener LF en scripts, pruebas y documentos. +- Mejoras de RPCS3 compartidas con YARG y Clone Hero en `lib/kiosk_runtime.sh`: + salida de audio por HDMI con regla de WirePlumber y volumen al arrancar + (`*_AUDIO_OUTPUT`, `*_AUDIO_VOLUME`), cuantum de PipeWire para bajar la latencia + (`*_PIPEWIRE_QUANTUM`, 128 por defecto), atajo Ctrl+Alt+Q para cerrar la app + (`*_EXIT_HOTKEY`, script comun `kiosk-exit-hotkey.py`), disco minimo por ruta + (`YARG_MIN_DISK_GB` y `CLONEHERO_MIN_DISK_GB`, 32 GB) y updaters que no descargan + si ya esta la ultima version (`update-yarg --force`, `update-clonehero --force`). +- `configure_kiosk_performance`: una sola implementacion de limites de tiempo real, + swappiness y cpupower para las tres rutas (antes copiada en cada una). ### Cambiado diff --git a/README.md b/README.md index d324dbd..91fb27d 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,8 @@ de shaders no caben en el `/home` de un disco de 16 GB. - Arch Linux ISO actual. - Maquina fisica o VM con UEFI habilitado. -- Disco de al menos 16 GB (32 GB para el camino RPCS3). +- Disco de al menos 32 GB en las tres rutas con juego (`YARG_MIN_DISK_GB`, + `CLONEHERO_MIN_DISK_GB` y `RPCS3_MIN_DISK_GB`); `install-cage-kiosk.sh` pide 16 GB. Esquema por defecto: ESP 512 MiB, root 8 GiB, swap 2 GiB y `/home` con el resto. En un disco de 16 GB `/home` queda de ~5.5 GB, en 32 GB de ~21.5 GB y en 128 GB de ~117.5 GB. Ajustable con `ESP_SIZE`, `ROOT_SIZE` y `SWAP_SIZE`. @@ -397,6 +398,8 @@ Canal de YARG: stable fijo, stable-latest o nightly? [stable/stable-latest/night `sudo update-yarg` respeta el canal instalado. En `stable` usa `YARG_URL`; en `stable-latest` consulta el latest estable; en `nightly` consulta el latest de `YARG-BleedingEdge` antes de descargar. +Si ya tienes la ultima version no descarga nada; `sudo update-yarg --force` la +reinstala igual. `update-clonehero` hace lo mismo (`--force` incluido). ## Uso Despues De Instalar Cage/YARG @@ -689,6 +692,15 @@ Variables de Cage/YARG: `false` permite usar la GPU disponible, recomendado para GPU passthrough. - `YARG_EXIT_MENU`: `always` muestra menu al salir de YARG; `restart` relanza YARG directo; `never` sale del wrapper. +- `YARG_AUDIO_OUTPUT` / `YARG_AUDIO_VOLUME`: salida de audio preferida (`hdmi` por + defecto, `analog` o `auto`) y volumen al arrancar (`0` a `1`, `1.0` por defecto). + Funcionan igual que `RPCS3_AUDIO_OUTPUT` / `RPCS3_AUDIO_VOLUME`. +- `YARG_PIPEWIRE_QUANTUM`: ciclo de PipeWire en muestras (`128` por defecto; vacio no + lo toca). Igual que `RPCS3_PIPEWIRE_QUANTUM`. +- `YARG_EXIT_HOTKEY`: `true` (por defecto) instala el atajo **Ctrl+Alt+Q** para cerrar + YARG desde el teclado; el wrapper vuelve entonces al menu de mantenimiento. + Comparte script con RPCS3 y Clone Hero (`/usr/local/bin/kiosk-exit-hotkey.py`). +- `YARG_MIN_DISK_GB`: disco minimo en GB. Por defecto `32`. Variables de Cage/Clone Hero: @@ -704,6 +716,14 @@ Variables de Cage/Clone Hero: - `CLONEHERO_FORCE_SOFTWARE_RENDER`: `true` fuerza llvmpipe/software render. - `CLONEHERO_EXIT_MENU`: `always` muestra menu al salir de Clone Hero; `restart` relanza Clone Hero directo; `never` sale del wrapper. +- `CLONEHERO_AUDIO_OUTPUT` / `CLONEHERO_AUDIO_VOLUME`: salida de audio preferida + (`hdmi` por defecto, `analog` o `auto`) y volumen al arrancar (`0` a `1`, `1.0` + por defecto). Funcionan igual que `RPCS3_AUDIO_OUTPUT` / `RPCS3_AUDIO_VOLUME`. +- `CLONEHERO_PIPEWIRE_QUANTUM`: ciclo de PipeWire en muestras (`128` por defecto; vacio + no lo toca). Igual que `RPCS3_PIPEWIRE_QUANTUM`. +- `CLONEHERO_EXIT_HOTKEY`: `true` (por defecto) instala el atajo **Ctrl+Alt+Q** para + cerrar Clone Hero desde el teclado (mismo script que YARG y RPCS3). +- `CLONEHERO_MIN_DISK_GB`: disco minimo en GB. Por defecto `32`. Variables de Cage/RPCS3: @@ -748,12 +768,12 @@ Variables de Cage/RPCS3: `analog` o `auto`. Crea una regla de WirePlumber en `~/.config/wireplumber/wireplumber.conf.d/` que sube la prioridad de esos sinks; si el dispositivo preferido no existe se usa el otro. Para cambiarlo despues, - edita o borra `51-rpcs3-audio-output.conf` y reinicia `cage-kiosk.service`. + edita o borra `51-kiosk-audio-output.conf` y reinicia `cage-kiosk.service`. - `RPCS3_AUDIO_VOLUME`: volumen de la salida de audio al arrancar, de `0` a `1` (`1.0` = 100 %, por defecto). Se fija con `wpctl` en cada arranque del wrapper y quita el silencio; vacio no lo toca. Util porque WirePlumber puede dejar un sink HDMI en 40 %. -- `RPCS3_EXIT_HOTKEY`: `true` (por defecto) instala `/usr/local/bin/rpcs3-exit-hotkey.py`, +- `RPCS3_EXIT_HOTKEY`: `true` (por defecto) instala `/usr/local/bin/kiosk-exit-hotkey.py`, que el wrapper arranca para cerrar RPCS3 con **Ctrl+Alt+Q** en el teclado; el wrapper vuelve entonces al menu de mantenimiento. Cage no procesa Alt+F4 y RPCS3 sin GUI no tiene atajo de teclado para cerrarse. Con un control no hace diff --git a/install-cage-clonehero.sh b/install-cage-clonehero.sh index 53e8689..31238b5 100644 --- a/install-cage-clonehero.sh +++ b/install-cage-clonehero.sh @@ -50,6 +50,16 @@ CLONEHERO_DATA_DIR="${CLONEHERO_DATA_DIR:-}" CLONEHERO_RESOLUTION="${CLONEHERO_RESOLUTION:-ask}" CLONEHERO_FORCE_SOFTWARE_RENDER="${CLONEHERO_FORCE_SOFTWARE_RENDER:-false}" CLONEHERO_EXIT_MENU="${CLONEHERO_EXIT_MENU:-always}" +# hdmi (por defecto), analog o auto: salida de audio preferida; volumen de 0 a 1 al arrancar. +CLONEHERO_AUDIO_OUTPUT="${CLONEHERO_AUDIO_OUTPUT:-hdmi}" +CLONEHERO_AUDIO_VOLUME="${CLONEHERO_AUDIO_VOLUME-1.0}" +# Tamano de ciclo de PipeWire en muestras (48 kHz): 128 = 2.7 ms, 1024 = 21 ms (el valor +# por defecto de PipeWire). Menos = menos latencia, pero mas carga de CPU. Vacio no lo toca. +CLONEHERO_PIPEWIRE_QUANTUM="${CLONEHERO_PIPEWIRE_QUANTUM-128}" +# true instala el atajo para cerrar Clone Hero con Ctrl+Alt+Q (teclado). +CLONEHERO_EXIT_HOTKEY="${CLONEHERO_EXIT_HOTKEY:-true}" +# Disco minimo en GB: las canciones y el /home no caben en un disco de 16 GB. +CLONEHERO_MIN_DISK_GB="${CLONEHERO_MIN_DISK_GB:-32}" source "$SCRIPT_DIR/lib/validation.sh" || { log_error "No se pudo importar validation.sh"; exit 1; } source "$SCRIPT_DIR/lib/partitioning.sh" || { log_error "No se pudo importar partitioning.sh"; exit 1; } @@ -158,6 +168,9 @@ ask_initial_questions() { echo -e "${YELLOW}Usuario kiosko:${NC} $KIOSK_USER" echo "" + validate_kiosk_audio_volume CLONEHERO_AUDIO_VOLUME "$CLONEHERO_AUDIO_VOLUME" || return 1 + validate_kiosk_pipewire_quantum CLONEHERO_PIPEWIRE_QUANTUM "$CLONEHERO_PIPEWIRE_QUANTUM" || return 1 + resolve_nvidia_choice CLONEHERO_RELEASE_CHANNEL="${CLONEHERO_RELEASE_CHANNEL,,}" @@ -251,8 +264,8 @@ main() { exit 1 fi - if ! check_disk "$DISK_DEVICE"; then - log_error "Disco invalido o insuficiente: se requiere al menos 16GB" + if ! check_disk "$DISK_DEVICE" "$CLONEHERO_MIN_DISK_GB"; then + log_error "Disco invalido o insuficiente: Clone Hero requiere al menos ${CLONEHERO_MIN_DISK_GB}GB" exit 1 fi @@ -346,6 +359,9 @@ main() { exit 1 fi + configure_kiosk_audio_output "${CLONEHERO_AUDIO_OUTPUT:-hdmi}" + install_kiosk_exit_hotkey "${CLONEHERO_EXIT_HOTKEY:-true}" CLONEHERO + if [[ -e "$CURSOR_PATH" ]]; then if ! install_custom_cursor "$CURSOR_PATH" "$KIOSK_USER"; then warn "Fallo en instalacion del cursor personalizado; se continuara con el cursor predeterminado." @@ -379,7 +395,7 @@ main() { exit 1 fi - if ! configure_clonehero_performance; then + if ! configure_kiosk_performance "Clone Hero"; then log_error "Fallo en optimizaciones de rendimiento para Clone Hero" exit 1 fi diff --git a/install-cage-rpcs3.sh b/install-cage-rpcs3.sh index 174d91c..5ae375d 100644 --- a/install-cage-rpcs3.sh +++ b/install-cage-rpcs3.sh @@ -174,16 +174,8 @@ ask_guided_configuration() { ;; esac - if [[ -n "$RPCS3_PIPEWIRE_QUANTUM" ]] && \ - { [[ ! "$RPCS3_PIPEWIRE_QUANTUM" =~ ^[0-9]+$ ]] || (( RPCS3_PIPEWIRE_QUANTUM < 32 || RPCS3_PIPEWIRE_QUANTUM > 2048 )); }; then - log_error "RPCS3_PIPEWIRE_QUANTUM invalido: $RPCS3_PIPEWIRE_QUANTUM. Use un numero entre 32 y 2048 (por ejemplo 128) o vacio." - return 1 - fi - - if [[ -n "$RPCS3_AUDIO_VOLUME" && ! "$RPCS3_AUDIO_VOLUME" =~ ^(0(\.[0-9]+)?|1(\.0+)?)$ ]]; then - log_error "RPCS3_AUDIO_VOLUME invalido: $RPCS3_AUDIO_VOLUME. Use un numero entre 0 y 1 (por ejemplo 1.0) o vacio." - return 1 - fi + validate_kiosk_pipewire_quantum RPCS3_PIPEWIRE_QUANTUM "$RPCS3_PIPEWIRE_QUANTUM" || return 1 + validate_kiosk_audio_volume RPCS3_AUDIO_VOLUME "$RPCS3_AUDIO_VOLUME" || return 1 if [[ -z "${RB3DX_CONFIG_PROFILE:-}" ]]; then local profile_answer="recommended" @@ -461,19 +453,19 @@ main() { exit 1 fi - if ! configure_rpcs3_performance; then + if ! configure_kiosk_performance RPCS3 "vm.max_map_count=2147483642"; then log_error "Fallo en optimizaciones de rendimiento para RPCS3" exit 1 fi - configure_rpcs3_audio_output + configure_kiosk_audio_output "${RPCS3_AUDIO_OUTPUT:-hdmi}" if ! install_rpcs3_update_script; then log_error "Fallo en instalacion del updater de RPCS3" exit 1 fi - install_rpcs3_exit_hotkey + install_kiosk_exit_hotkey "${RPCS3_EXIT_HOTKEY:-true}" RPCS3 install_rpcs3_midi_config install_rpcs3_input_config diff --git a/install-cage-yarg.sh b/install-cage-yarg.sh index b7cadcd..4effcc9 100644 --- a/install-cage-yarg.sh +++ b/install-cage-yarg.sh @@ -48,6 +48,16 @@ YARG_PERSISTENT_DATA_DIR="${YARG_PERSISTENT_DATA_DIR:-}" YARG_RESOLUTION="${YARG_RESOLUTION:-ask}" YARG_FORCE_SOFTWARE_RENDER="${YARG_FORCE_SOFTWARE_RENDER:-false}" YARG_EXIT_MENU="${YARG_EXIT_MENU:-always}" +# hdmi (por defecto), analog o auto: salida de audio preferida; volumen de 0 a 1 al arrancar. +YARG_AUDIO_OUTPUT="${YARG_AUDIO_OUTPUT:-hdmi}" +YARG_AUDIO_VOLUME="${YARG_AUDIO_VOLUME-1.0}" +# Tamano de ciclo de PipeWire en muestras (48 kHz): 128 = 2.7 ms, 1024 = 21 ms (el valor +# por defecto de PipeWire). Menos = menos latencia, pero mas carga de CPU. Vacio no lo toca. +YARG_PIPEWIRE_QUANTUM="${YARG_PIPEWIRE_QUANTUM-128}" +# true instala el atajo para cerrar YARG con Ctrl+Alt+Q (teclado). +YARG_EXIT_HOTKEY="${YARG_EXIT_HOTKEY:-true}" +# Disco minimo en GB: las canciones y el /home no caben en un disco de 16 GB. +YARG_MIN_DISK_GB="${YARG_MIN_DISK_GB:-32}" YARG_RELEASE_CHANNEL="${YARG_RELEASE_CHANNEL:-ask}" YARG_STABLE_API_URL="${YARG_STABLE_API_URL:-https://api.github.com/repos/YARC-Official/YARG/releases/latest}" YARG_STABLE_ASSET_REGEX="${YARG_STABLE_ASSET_REGEX:-linux.*(x86_64|x64|64).*\\.zip}" @@ -162,6 +172,9 @@ ask_initial_questions() { echo -e "${YELLOW}Usuario kiosko:${NC} $KIOSK_USER" echo "" + validate_kiosk_audio_volume YARG_AUDIO_VOLUME "$YARG_AUDIO_VOLUME" || return 1 + validate_kiosk_pipewire_quantum YARG_PIPEWIRE_QUANTUM "$YARG_PIPEWIRE_QUANTUM" || return 1 + resolve_nvidia_choice YARG_RELEASE_CHANNEL="${YARG_RELEASE_CHANNEL,,}" @@ -257,8 +270,8 @@ main() { exit 1 fi - if ! check_disk "$DISK_DEVICE"; then - log_error "Disco invalido o insuficiente: se requiere al menos 16GB" + if ! check_disk "$DISK_DEVICE" "$YARG_MIN_DISK_GB"; then + log_error "Disco invalido o insuficiente: YARG requiere al menos ${YARG_MIN_DISK_GB}GB" exit 1 fi @@ -352,6 +365,9 @@ main() { exit 1 fi + configure_kiosk_audio_output "${YARG_AUDIO_OUTPUT:-hdmi}" + install_kiosk_exit_hotkey "${YARG_EXIT_HOTKEY:-true}" YARG + if [[ -e "$CURSOR_PATH" ]]; then if ! install_custom_cursor "$CURSOR_PATH" "$KIOSK_USER"; then warn "Fallo en instalacion del cursor personalizado; se continuara con el cursor predeterminado." @@ -385,7 +401,7 @@ main() { exit 1 fi - if ! configure_yarg_performance; then + if ! configure_kiosk_performance YARG; then log_error "Fallo en optimizaciones de rendimiento para YARG" exit 1 fi diff --git a/lib/cage.sh b/lib/cage.sh index 9f9ddd7..b0e8fb4 100644 --- a/lib/cage.sh +++ b/lib/cage.sh @@ -341,7 +341,7 @@ install_cage_wrapper() { install_kiosk_menu "YARG" "Volver a YARG" "$yarg_update_label" "/usr/local/bin/update-yarg" { - kiosk_wrapper_prelude "run-yarg" "/home/$KIOSK_USER" "${YARG_FORCE_SOFTWARE_RENDER:-false}" + kiosk_wrapper_prelude "run-yarg" "/home/$KIOSK_USER" "${YARG_FORCE_SOFTWARE_RENDER:-false}" "${YARG_AUDIO_VOLUME-}" "${YARG_PIPEWIRE_QUANTUM-}" cat <<'WRAPPER' YARG_SCREEN_WIDTH="__YARG_SCREEN_WIDTH__" @@ -369,6 +369,7 @@ wait_for_alsa_default() { return 1 } +start_kiosk_exit_hotkey /opt/YARG/ start_kiosk_audio YARG echo "run-yarg: esperando ALSA default via PipeWire" >&2 diff --git a/lib/clonehero.sh b/lib/clonehero.sh index 72de924..2607d1b 100644 --- a/lib/clonehero.sh +++ b/lib/clonehero.sh @@ -115,6 +115,8 @@ install_clonehero() { run_quiet arch-chroot /mnt rm -rf /opt/CloneHero/.new run_quiet arch-chroot /mnt find /opt/CloneHero -maxdepth 2 -type f \( -iname 'Clone Hero*' -o -iname 'CloneHero*' -o -iname 'clonehero' -o -iname '*.AppImage' \) -exec chmod +x {} + run_quiet arch-chroot /mnt mkdir -p "$CLONEHERO_SONGS_DIR" "$CLONEHERO_DATA_DIR" + # Marca de la URL instalada: update-clonehero no descarga si ya esta la ultima version. + printf '%s\n' "$CLONEHERO_URL" > /mnt/opt/CloneHero/.install-url run_quiet arch-chroot /mnt chown -R "$KIOSK_USER:$KIOSK_USER" /opt/CloneHero "$CLONEHERO_DATA_DIR" rm -f "$package_file" } @@ -185,45 +187,6 @@ EOF fi } -configure_clonehero_performance() { - log "Aplicando optimizaciones de rendimiento para Clone Hero" - - mkdir -p /mnt/etc/security/limits.d /mnt/etc/sysctl.d /mnt/etc/default - - cat > /mnt/etc/security/limits.d/99-clonehero.conf << EOF -$KIOSK_USER - rtprio 99 -$KIOSK_USER - memlock unlimited -$KIOSK_USER - nice -20 -EOF - - echo 'vm.swappiness=10' > /mnt/etc/sysctl.d/99-clonehero.conf - - cat > /mnt/etc/default/cpupower << 'EOF' -# Versiones antiguas del servicio cpupower leen este archivo (en minusculas o -# mayusculas). Se escriben ambas formas. -GOVERNOR='performance' -MIN_FREQ='' -MAX_FREQ='' -governor='performance' -min_freq='' -max_freq='' -EOF - - # Los paquetes recientes de cpupower leen su configuracion de este otro - # archivo (EnvironmentFile de la unidad), no de /etc/default/cpupower: sin ese - # archivo el servicio termina con exito pero el gobernador queda en schedutil. - cat > /mnt/etc/default/cpupower-service.conf << 'EOF' -GOVERNOR='performance' -MIN_FREQ='' -MAX_FREQ='' -EOF - - if ! run_quiet arch-chroot /mnt systemctl enable cpupower.service; then - log_error "Fallo al habilitar cpupower.service" - return 1 - fi -} - install_clonehero_update_script() { normalize_clonehero_songs_dir @@ -242,6 +205,10 @@ INSTALL_DIR="/opt/CloneHero" SONGS_DIR="$CLONEHERO_SONGS_DIR" PACKAGE_FILE="/tmp/CloneHero.download" OWNER="$KIOSK_USER" +# URL instalada; el nombre lleva la version. --force reinstala igual. +URL_MARK="\$INSTALL_DIR/.install-url" +FORCE=false +[[ "\${1:-}" == "--force" ]] && FORCE=true if [[ \${EUID} -ne 0 ]]; then echo "Este script debe ejecutarse como root." >&2 @@ -278,6 +245,12 @@ if [[ "\$CLONEHERO_RELEASE_CHANNEL" == "latest" ]]; then CLONEHERO_URL="\$(resolve_latest_release_url)" fi +if [[ "\$FORCE" != "true" && -r "\$URL_MARK" && "\$(cat "\$URL_MARK")" == "\$CLONEHERO_URL" ]]; then + echo "Clone Hero ya esta en la ultima version (\$(basename "\$CLONEHERO_URL")); no se descarga nada." + echo "Usa update-clonehero --force para reinstalarlo igual." + exit 0 +fi + echo "Descargando Clone Hero desde: \$CLONEHERO_URL" curl -fsSL --retry 3 --retry-delay 2 -o "\$PACKAGE_FILE" "\$CLONEHERO_URL" rm -rf "\$INSTALL_DIR/.new" @@ -312,6 +285,7 @@ else fi rm -rf "\$INSTALL_DIR/.new" find "\$INSTALL_DIR" -maxdepth 2 -type f \( -iname 'Clone Hero*' -o -iname 'CloneHero*' -o -iname 'clonehero' -o -iname '*.AppImage' \) -exec chmod +x {} + +printf '%s\n' "\$CLONEHERO_URL" > "\$URL_MARK" chown -R "\$OWNER:\$OWNER" "\$INSTALL_DIR" "\$SONGS_DIR" rm -f "\$PACKAGE_FILE" @@ -599,7 +573,7 @@ install_clonehero_cage_wrapper() { install_kiosk_menu "Clone Hero" "Volver a Clone Hero" "Actualizar Clone Hero" "/usr/local/bin/update-clonehero" { - kiosk_wrapper_prelude "run-clonehero" "/home/$KIOSK_USER" "${CLONEHERO_FORCE_SOFTWARE_RENDER:-false}" + kiosk_wrapper_prelude "run-clonehero" "/home/$KIOSK_USER" "${CLONEHERO_FORCE_SOFTWARE_RENDER:-false}" "${CLONEHERO_AUDIO_VOLUME-}" "${CLONEHERO_PIPEWIRE_QUANTUM-}" cat <<'WRAPPER' CLONEHERO_SCREEN_WIDTH="__CLONEHERO_SCREEN_WIDTH__" @@ -607,6 +581,7 @@ CLONEHERO_SCREEN_HEIGHT="__CLONEHERO_SCREEN_HEIGHT__" CLONEHERO_EXIT_MENU="__CLONEHERO_EXIT_MENU__" CLONEHERO_DATA_DIR="__CLONEHERO_DATA_DIR__" +start_kiosk_exit_hotkey /opt/CloneHero/ start_kiosk_audio "Clone Hero" find_clonehero_bin() { diff --git a/lib/kiosk_runtime.sh b/lib/kiosk_runtime.sh index 012692b..d6bc04b 100644 --- a/lib/kiosk_runtime.sh +++ b/lib/kiosk_runtime.sh @@ -5,19 +5,347 @@ # Piezas comunes de los kioscos Cage (YARG, Clone Hero y RPCS3): # - kiosk_wrapper_prelude: prologo del wrapper run-.sh (entorno, DBus, # PipeWire). Cada app agrega su bloque especifico despues. +# - configure_kiosk_audio_output / cuantum de PipeWire / volumen: audio comun. +# - install_kiosk_exit_hotkey: atajo Ctrl+Alt+Q para cerrar la app. +# - configure_kiosk_performance: limites, sysctl y cpupower. # - install_kiosk_menu: menu de mantenimiento /usr/local/bin/kiosk-menu.sh. # - install_cage_service: servicio cage-kiosk.service. # Un arreglo en estas piezas aplica a las tres apps. # ============================================================================= -# Prologo del wrapper. Uso: kiosk_wrapper_prelude +# Elige por que salida suenan los kioscos (hdmi/dp, analog o auto). Se logra +# subiendo la prioridad de los sinks HDMI/DP o analogicos con una regla de +# WirePlumber en el home del usuario; si el dispositivo preferido no existe, +# WirePlumber cae solo al otro. El wrapper arranca wireplumber como el usuario, +# asi que toma ~/.config/wireplumber/wireplumber.conf.d/. +# Uso: configure_kiosk_audio_output [hdmi|dp|analog|auto] (por defecto hdmi) +configure_kiosk_audio_output() { + local output="${1:-hdmi}" + local conf_dir="/mnt/home/$KIOSK_USER/.config/wireplumber/wireplumber.conf.d" + local pattern + + case "${output,,}" in + hdmi|dp) + pattern='~alsa_output.*hdmi.*' + ;; + analog) + pattern='~alsa_output.*analog.*' + ;; + auto) + log "Salida de audio automatica (auto)" + return 0 + ;; + *) + warn "Salida de audio invalida: $output; se deja la salida automatica." + return 0 + ;; + esac + + log "Priorizando la salida de audio: ${output,,}" + mkdir -p "$conf_dir" + + cat > "$conf_dir/51-kiosk-audio-output.conf" << EOF_CONF +monitor.alsa.rules = [ + { + matches = [ { node.name = "$pattern" } ] + actions = { update-props = { priority.session = 3000, priority.driver = 3000 } } + } +] +EOF_CONF + + run_quiet arch-chroot /mnt chown -R "$KIOSK_USER:$KIOSK_USER" "/home/$KIOSK_USER/.config" +} + +# Valida un volumen de audio (0 a 1, o vacio). Uso: validate_kiosk_audio_volume +validate_kiosk_audio_volume() { + local name="$1" value="$2" + + if [[ -n "$value" && ! "$value" =~ ^(0(\.[0-9]+)?|1(\.0+)?)$ ]]; then + log_error "$name invalido: $value. Use un numero entre 0 y 1 (por ejemplo 1.0) o vacio." + return 1 + fi +} + +# Valida el cuantum de PipeWire (32 a 2048 muestras, o vacio). +# Uso: validate_kiosk_pipewire_quantum +validate_kiosk_pipewire_quantum() { + local name="$1" value="$2" + + if [[ -n "$value" ]] && { [[ ! "$value" =~ ^[0-9]+$ ]] || (( value < 32 || value > 2048 )); }; then + log_error "$name invalido: $value. Use un numero entre 32 y 2048 (por ejemplo 128) o vacio." + return 1 + fi +} + +# Limites de tiempo real, swappiness y gobernador cpupower=performance para la app. +# Uso: configure_kiosk_performance [clave=valor de sysctl...] +configure_kiosk_performance() { + local app="$1" slug line + shift + slug="${app,,}" + slug="${slug// /}" + + log "Aplicando optimizaciones de rendimiento para $app" + + mkdir -p /mnt/etc/security/limits.d /mnt/etc/sysctl.d /mnt/etc/default + + cat > "/mnt/etc/security/limits.d/99-$slug.conf" << EOF_LIMITS +$KIOSK_USER - rtprio 99 +$KIOSK_USER - memlock unlimited +$KIOSK_USER - nice -20 +EOF_LIMITS + + echo 'vm.swappiness=10' > "/mnt/etc/sysctl.d/99-$slug.conf" + for line in "$@"; do + echo "$line" >> "/mnt/etc/sysctl.d/99-$slug.conf" + done + + cat > /mnt/etc/default/cpupower << 'EOF_CPUPOWER' +# Versiones antiguas del servicio cpupower leen este archivo (en minusculas o +# mayusculas). Se escriben ambas formas. +GOVERNOR='performance' +MIN_FREQ='' +MAX_FREQ='' +governor='performance' +min_freq='' +max_freq='' +EOF_CPUPOWER + + # Los paquetes recientes de cpupower leen su configuracion de este otro + # archivo (EnvironmentFile de la unidad), no de /etc/default/cpupower: sin ese + # archivo el servicio termina con exito pero el gobernador queda en schedutil. + cat > /mnt/etc/default/cpupower-service.conf << 'EOF_CPUPOWER_SERVICE' +GOVERNOR='performance' +MIN_FREQ='' +MAX_FREQ='' +EOF_CPUPOWER_SERVICE + + if ! run_quiet arch-chroot /mnt systemctl enable cpupower.service; then + log_error "Fallo al habilitar cpupower.service" + return 1 + fi +} + +# Atajo Ctrl+Alt+Q para cerrar la app: script comun que lee los teclados y termina +# los procesos que le pase el wrapper (start_kiosk_exit_hotkey). +read -r -d '' KIOSK_EXIT_HOTKEY_TEMPLATE <<'TEMPLATE' || true +#!/usr/bin/env python3 +"""Cierra la app del kiosko con un atajo, sin depender del compositor ni de la ventana. + +Lee los teclados directamente (como evmapy/hotkeygen en Batocera): Ctrl + Alt + Q. +Al detectar la combinacion termina los procesos indicados; el wrapper del kiosko +vuelve entonces al menu de mantenimiento. Los instrumentos (guitarras, baterias) +se ignoran. + +Uso: kiosk-exit-hotkey.py [--list] PROCESO... + +Cada PROCESO es un nombre exacto (pkill -x) o, si empieza por /, un prefijo de la +ruta del ejecutable (p. ej. /opt/YARG/). --list muestra que dispositivos detecta. +""" +import glob +import os +import re +import select +import struct +import subprocess +import sys +import time + +EV_KEY = 1 +# struct input_event en x86_64: timeval (2 long), type, code, value. +EVENT = struct.Struct("llHHi") + +KEY_Q, KEY_LEFTCTRL, KEY_LEFTALT, KEY_RIGHTCTRL, KEY_RIGHTALT = 16, 29, 56, 97, 100 + +CTRL = {KEY_LEFTCTRL, KEY_RIGHTCTRL} +ALT = {KEY_LEFTALT, KEY_RIGHTALT} +EXCLUDED_NAMES = ("santroller", "guitar", "drum", "harmonix", "rock band", "keytar") +RESCAN_SECONDS = 5 +COOLDOWN_SECONDS = 3 +GRACE_SECONDS = 3 + + +def read_sysfs(event, name): + try: + with open("/sys/class/input/%s/device/%s" % (event, name)) as f: + return f.read().strip() + except OSError: + return "" + + +def key_capabilities(event): + """Devuelve la mascara de teclas/botones del dispositivo como entero.""" + mask = 0 + words = read_sysfs(event, "capabilities/key").split() + for i, word in enumerate(reversed(words)): + mask |= int(word, 16) << (64 * i) + return mask + + +def classify(mask, name): + """Tipos de atajo que admite un dispositivo: 'kbd' si es un teclado.""" + if any(excluded in name.lower() for excluded in EXCLUDED_NAMES): + return set() + has = lambda code: (mask >> code) & 1 + kinds = set() + if has(KEY_Q) and has(KEY_LEFTCTRL) and has(KEY_LEFTALT): + kinds.add("kbd") + return kinds + + +def combo_pressed(kinds, held): + if "kbd" in kinds and held & CTRL and held & ALT and KEY_Q in held: + return "teclado" + return None + + +def scan(devices, opened): + for path in sorted(glob.glob("/dev/input/event*")): + if path in opened: + continue + event = os.path.basename(path) + name = read_sysfs(event, "name") + kinds = classify(key_capabilities(event), name) + if not kinds: + continue + try: + fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + except OSError: + continue + opened.add(path) + devices[fd] = {"path": path, "name": name, "kinds": kinds, "held": set()} + print("kiosk-exit-hotkey: vigilando %s (%s) [%s]" % (path, name, ",".join(sorted(kinds))), flush=True) + + +def close_device(devices, opened, fd): + opened.discard(devices[fd]["path"]) + try: + os.close(fd) + except OSError: + pass + del devices[fd] + + +def match_args(target): + """Argumentos de pgrep/pkill para un objetivo: ruta (prefijo) o nombre exacto.""" + if target.startswith("/"): + return ["-f", "^" + re.escape(target)] + return ["-x", target] + + +def running(targets): + """True si queda algun proceso vivo con esos nombres (un zombi no cuenta).""" + for target in targets: + out = subprocess.run(["pgrep"] + match_args(target), capture_output=True, text=True).stdout.split() + for pid in out: + try: + with open("/proc/%s/stat" % pid) as f: + state = f.read().rsplit(")", 1)[1].split()[0] + except (OSError, IndexError): + continue + if state != "Z": + return True + return False + + +def terminate_app(source, targets, grace=GRACE_SECONDS): + print("kiosk-exit-hotkey: combinacion de %s; cerrando la app" % source, flush=True) + for target in targets: + subprocess.run(["pkill"] + match_args(target), check=False) + + # Algunas apps (la GUI de RPCS3) atrapan SIGTERM y no siempre salen: + # si pasado el plazo sigue vivo, se escala a SIGKILL. + deadline = time.monotonic() + grace + while time.monotonic() < deadline: + if not running(targets): + return + time.sleep(0.25) + print("kiosk-exit-hotkey: la app no salio con SIGTERM; forzando SIGKILL", flush=True) + for target in targets: + subprocess.run(["pkill", "-9"] + match_args(target), check=False) + + +def main(): + devices, opened = {}, set() + targets = [arg for arg in sys.argv[1:] if not arg.startswith("--")] + + if "--list" in sys.argv: + scan(devices, opened) + if not devices: + print("Sin dispositivos compatibles.") + return 0 + + if not targets: + print("Uso: kiosk-exit-hotkey.py [--list] PROCESO...", file=sys.stderr) + return 2 + + last_scan = last_fire = 0.0 + while True: + now = time.monotonic() + if now - last_scan >= RESCAN_SECONDS: + scan(devices, opened) + last_scan = now + + if not devices: + time.sleep(1) + continue + + ready, _, _ = select.select(list(devices), [], [], 1.0) + for fd in ready: + dev = devices[fd] + try: + data = os.read(fd, EVENT.size * 64) + except BlockingIOError: + continue + except OSError: + close_device(devices, opened, fd) + continue + + for offset in range(0, len(data) - EVENT.size + 1, EVENT.size): + _, _, etype, code, value = EVENT.unpack_from(data, offset) + if etype != EV_KEY: + continue + if value: + dev["held"].add(code) + else: + dev["held"].discard(code) + + source = combo_pressed(dev["kinds"], dev["held"]) if value == 1 else None + if source and time.monotonic() - last_fire >= COOLDOWN_SECONDS: + last_fire = time.monotonic() + terminate_app(source, targets) + + +if __name__ == "__main__": + sys.exit(main()) +TEMPLATE + +# Instala /usr/local/bin/kiosk-exit-hotkey.py. Uso: install_kiosk_exit_hotkey +install_kiosk_exit_hotkey() { + local enabled="${1:-true}" app="${2:-la app}" + + if [[ "$enabled" != "true" ]]; then + log "Atajo de salida omitido (${app}_EXIT_HOTKEY=false)" + return 0 + fi + + log "Instalando atajo de salida de $app (Ctrl+Alt+Q)" + mkdir -p /mnt/usr/local/bin + printf '%s\n' "$KIOSK_EXIT_HOTKEY_TEMPLATE" > /mnt/usr/local/bin/kiosk-exit-hotkey.py + chmod 755 /mnt/usr/local/bin/kiosk-exit-hotkey.py +} + +# Prologo del wrapper. Uso: kiosk_wrapper_prelude [volumen] [cuantum] # etiqueta prefijo de los mensajes (p. ej. run-yarg) # home valor por defecto de HOME # render-software true/false (o un marcador que se sustituye despues) +# volumen volumen de la salida al arrancar, de 0 a 1 (o un marcador); +# vacio no lo toca +# cuantum tamano de ciclo de PipeWire en muestras (32 a 2048); vacio no lo toca # Deja definidas las funciones wait_for_path, wait_for_pulse_sink y # start_kiosk_audio . kiosk_wrapper_prelude() { - local tag="$1" home="$2" software="$3" + local tag="$1" home="$2" software="$3" volume="${4-}" quantum="${5-}" local out read -r -d '' out <<'PRELUDE' || true @@ -27,6 +355,8 @@ set -euo pipefail echo "__KIOSK_TAG__: iniciado como $(id -un) pid=$$" >&2 KIOSK_FORCE_SOFTWARE_RENDER="__KIOSK_SOFTWARE__" +KIOSK_AUDIO_VOLUME="__KIOSK_AUDIO_VOLUME__" +KIOSK_PIPEWIRE_QUANTUM="__KIOSK_PIPEWIRE_QUANTUM__" case "${KIOSK_FORCE_SOFTWARE_RENDER,,}" in true|yes|si|1) @@ -154,12 +484,41 @@ start_kiosk_audio() { echo "__KIOSK_TAG__: esperando sink Pulse/PipeWire" >&2 wait_for_pulse_sink 50 || \ echo "Aviso: no se encontro un sink Pulse/PipeWire antes de iniciar $app." >&2 + + # WirePlumber recuerda un volumen bajo (40 %) en algunos equipos; se fija el + # volumen de la salida por defecto y se quita el silencio. + if [[ -n "$KIOSK_AUDIO_VOLUME" ]] && command -v wpctl >/dev/null 2>&1; then + wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1 || true + wpctl set-volume @DEFAULT_AUDIO_SINK@ "$KIOSK_AUDIO_VOLUME" >/dev/null 2>&1 || true + fi + + # PipeWire trabaja por defecto con ciclos de 1024 muestras (21 ms a 48 kHz) y + # cada etapa de un microfono (dispositivo, fuente mono, lectura de la app) puede + # sumar uno. Se fuerza un cuantum menor para bajar la latencia de los + # microfonos y del audio. Es de ejecucion, asi que se aplica en cada arranque. + if [[ -n "$KIOSK_PIPEWIRE_QUANTUM" ]] && command -v pw-metadata >/dev/null 2>&1; then + pw-metadata -n settings 0 clock.force-quantum "$KIOSK_PIPEWIRE_QUANTUM" >/dev/null 2>&1 || true + fi +} + +# Arranca el atajo Ctrl+Alt+Q (si esta instalado) para cerrar la app. Cada argumento +# es un proceso a terminar: un nombre exacto, o una ruta absoluta (prefijo del +# ejecutable, p. ej. /opt/YARG/). +start_kiosk_exit_hotkey() { + local script=/usr/local/bin/kiosk-exit-hotkey.py + + [[ -f "$script" ]] && command -v python3 >/dev/null 2>&1 || return 0 + pgrep -u "$(id -u)" -f "$script" >/dev/null 2>&1 && return 0 + + python3 "$script" "$@" 2>&1 | sed 's/^/[exit-hotkey] /' >&2 & } PRELUDE out="${out//__KIOSK_TAG__/"$tag"}" out="${out//__KIOSK_HOME__/"$home"}" out="${out//__KIOSK_SOFTWARE__/"$software"}" + out="${out//__KIOSK_AUDIO_VOLUME__/"$volume"}" + out="${out//__KIOSK_PIPEWIRE_QUANTUM__/"$quantum"}" printf '%s\n' "$out" } diff --git a/lib/rpcs3.sh b/lib/rpcs3.sh index afec9b5..e1cda6a 100644 --- a/lib/rpcs3.sh +++ b/lib/rpcs3.sh @@ -343,90 +343,6 @@ EOF fi } -# Elige por que salida suena RPCS3 (RPCS3_AUDIO_OUTPUT): hdmi/dp (por defecto), -# analog o auto (deja la eleccion de WirePlumber). Se logra subiendo la -# prioridad de los sinks HDMI/DP o analogicos con una regla de WirePlumber en -# el home del usuario; si el dispositivo preferido no existe, WirePlumber cae -# solo al otro. El wrapper arranca wireplumber como el usuario, asi que toma -# ~/.config/wireplumber/wireplumber.conf.d/. -configure_rpcs3_audio_output() { - local output="${RPCS3_AUDIO_OUTPUT:-hdmi}" - local conf_dir="/mnt/home/$KIOSK_USER/.config/wireplumber/wireplumber.conf.d" - local pattern - - case "${output,,}" in - hdmi|dp) - pattern='~alsa_output.*hdmi.*' - ;; - analog) - pattern='~alsa_output.*analog.*' - ;; - auto) - log "Salida de audio automatica (RPCS3_AUDIO_OUTPUT=auto)" - return 0 - ;; - *) - warn "RPCS3_AUDIO_OUTPUT invalido: $output; se deja la salida automatica." - return 0 - ;; - esac - - log "Priorizando la salida de audio: ${output,,}" - mkdir -p "$conf_dir" - - cat > "$conf_dir/51-rpcs3-audio-output.conf" << EOF_CONF -monitor.alsa.rules = [ - { - matches = [ { node.name = "$pattern" } ] - actions = { update-props = { priority.session = 3000, priority.driver = 3000 } } - } -] -EOF_CONF - - run_quiet arch-chroot /mnt chown -R "$KIOSK_USER:$KIOSK_USER" "/home/$KIOSK_USER/.config" -} - -configure_rpcs3_performance() { - log "Aplicando optimizaciones de rendimiento para RPCS3" - - mkdir -p /mnt/etc/security/limits.d /mnt/etc/sysctl.d /mnt/etc/default - - cat > /mnt/etc/security/limits.d/99-rpcs3.conf << EOF -$KIOSK_USER - rtprio 99 -$KIOSK_USER - memlock unlimited -$KIOSK_USER - nice -20 -EOF - - echo 'vm.swappiness=10' > /mnt/etc/sysctl.d/99-rpcs3.conf - # RPCS3 abre muchos mapeos de memoria (PPU/SPU); el default de Linux no alcanza. - echo 'vm.max_map_count=2147483642' >> /mnt/etc/sysctl.d/99-rpcs3.conf - - cat > /mnt/etc/default/cpupower << 'EOF' -# Versiones antiguas del servicio cpupower leen este archivo (en minusculas o -# mayusculas). Se escriben ambas formas. -GOVERNOR='performance' -MIN_FREQ='' -MAX_FREQ='' -governor='performance' -min_freq='' -max_freq='' -EOF - - # Los paquetes recientes de cpupower leen su configuracion de este otro - # archivo (EnvironmentFile de la unidad), no de /etc/default/cpupower: sin ese - # archivo el servicio termina con exito pero el gobernador queda en schedutil. - cat > /mnt/etc/default/cpupower-service.conf << 'EOF' -GOVERNOR='performance' -MIN_FREQ='' -MAX_FREQ='' -EOF - - if ! run_quiet arch-chroot /mnt systemctl enable cpupower.service; then - log_error "Fallo al habilitar cpupower.service" - return 1 - fi -} - # --------------------------------------------------------------------------- # Plantillas # --------------------------------------------------------------------------- @@ -764,184 +680,6 @@ if __name__ == "__main__": sys.exit(main(sys.argv[1:])) TEMPLATE -read -r -d '' RPCS3_EXIT_HOTKEY_TEMPLATE <<'TEMPLATE' || true -#!/usr/bin/env python3 -"""Cierra RPCS3 con un atajo, sin depender del compositor ni de la ventana. - -Lee los teclados directamente (como evmapy/hotkeygen en Batocera): Ctrl + Alt + Q. -Al detectar la combinacion termina RPCS3; el wrapper del kiosko vuelve entonces -al menu de mantenimiento. Con un control no hace falta: su boton Guide abre el -menu de RPCS3, que trae la opcion de salir del juego. Los instrumentos -(guitarras, baterias) se ignoran. - -Uso: rpcs3-exit-hotkey.py [--list] (--list muestra que dispositivos detecta) -""" -import glob -import os -import select -import struct -import subprocess -import sys -import time - -EV_KEY = 1 -# struct input_event en x86_64: timeval (2 long), type, code, value. -EVENT = struct.Struct("llHHi") - -KEY_Q, KEY_LEFTCTRL, KEY_LEFTALT, KEY_RIGHTCTRL, KEY_RIGHTALT = 16, 29, 56, 97, 100 - -CTRL = {KEY_LEFTCTRL, KEY_RIGHTCTRL} -ALT = {KEY_LEFTALT, KEY_RIGHTALT} -EXCLUDED_NAMES = ("santroller", "guitar", "drum", "harmonix", "rock band", "keytar") -TARGETS = ("AppRun.wrapped", "rpcs3") -RESCAN_SECONDS = 5 -COOLDOWN_SECONDS = 3 -GRACE_SECONDS = 3 - - -def read_sysfs(event, name): - try: - with open("/sys/class/input/%s/device/%s" % (event, name)) as f: - return f.read().strip() - except OSError: - return "" - - -def key_capabilities(event): - """Devuelve la mascara de teclas/botones del dispositivo como entero.""" - mask = 0 - words = read_sysfs(event, "capabilities/key").split() - for i, word in enumerate(reversed(words)): - mask |= int(word, 16) << (64 * i) - return mask - - -def classify(mask, name): - """Tipos de atajo que admite un dispositivo: 'kbd' si es un teclado.""" - if any(excluded in name.lower() for excluded in EXCLUDED_NAMES): - return set() - has = lambda code: (mask >> code) & 1 - kinds = set() - if has(KEY_Q) and has(KEY_LEFTCTRL) and has(KEY_LEFTALT): - kinds.add("kbd") - return kinds - - -def combo_pressed(kinds, held): - if "kbd" in kinds and held & CTRL and held & ALT and KEY_Q in held: - return "teclado" - return None - - -def scan(devices, opened): - for path in sorted(glob.glob("/dev/input/event*")): - if path in opened: - continue - event = os.path.basename(path) - name = read_sysfs(event, "name") - kinds = classify(key_capabilities(event), name) - if not kinds: - continue - try: - fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) - except OSError: - continue - opened.add(path) - devices[fd] = {"path": path, "name": name, "kinds": kinds, "held": set()} - print("rpcs3-exit-hotkey: vigilando %s (%s) [%s]" % (path, name, ",".join(sorted(kinds))), flush=True) - - -def close_device(devices, opened, fd): - opened.discard(devices[fd]["path"]) - try: - os.close(fd) - except OSError: - pass - del devices[fd] - - -def running(targets): - """True si queda algun proceso vivo con esos nombres (un zombi no cuenta).""" - for target in targets: - out = subprocess.run(["pgrep", "-x", target], capture_output=True, text=True).stdout.split() - for pid in out: - try: - with open("/proc/%s/stat" % pid) as f: - state = f.read().rsplit(")", 1)[1].split()[0] - except (OSError, IndexError): - continue - if state != "Z": - return True - return False - - -def terminate_rpcs3(source, targets=TARGETS, grace=GRACE_SECONDS): - print("rpcs3-exit-hotkey: combinacion de %s; cerrando RPCS3" % source, flush=True) - for target in targets: - subprocess.run(["pkill", "-x", target], check=False) - - # La GUI de RPCS3 atrapa SIGTERM y no siempre sale (el juego con --no-gui si): - # si pasado el plazo sigue vivo, se escala a SIGKILL. - deadline = time.monotonic() + grace - while time.monotonic() < deadline: - if not running(targets): - return - time.sleep(0.25) - print("rpcs3-exit-hotkey: RPCS3 no salio con SIGTERM; forzando SIGKILL", flush=True) - for target in targets: - subprocess.run(["pkill", "-9", "-x", target], check=False) - - -def main(): - devices, opened = {}, set() - - if "--list" in sys.argv: - scan(devices, opened) - if not devices: - print("Sin dispositivos compatibles.") - return 0 - - last_scan = last_fire = 0.0 - while True: - now = time.monotonic() - if now - last_scan >= RESCAN_SECONDS: - scan(devices, opened) - last_scan = now - - if not devices: - time.sleep(1) - continue - - ready, _, _ = select.select(list(devices), [], [], 1.0) - for fd in ready: - dev = devices[fd] - try: - data = os.read(fd, EVENT.size * 64) - except BlockingIOError: - continue - except OSError: - close_device(devices, opened, fd) - continue - - for offset in range(0, len(data) - EVENT.size + 1, EVENT.size): - _, _, etype, code, value = EVENT.unpack_from(data, offset) - if etype != EV_KEY: - continue - if value: - dev["held"].add(code) - else: - dev["held"].discard(code) - - source = combo_pressed(dev["kinds"], dev["held"]) if value == 1 else None - if source and time.monotonic() - last_fire >= COOLDOWN_SECONDS: - last_fire = time.monotonic() - terminate_rpcs3(source) - - -if __name__ == "__main__": - sys.exit(main()) -TEMPLATE - read -r -d '' RPCS3_WRAPPER_BODY <<'TEMPLATE' || true RPCS3_GAMES_DIR="__RPCS3_GAMES_DIR__" RPCS3_GAME_PATH="__RPCS3_GAME_PATH__" @@ -950,8 +688,6 @@ RPCS3_EXIT_MENU="__RPCS3_EXIT_MENU__" RPCS3_MIDI_DRUMS="__RPCS3_MIDI_DRUMS__" RPCS3_PAD_AUTO="__RPCS3_PAD_AUTO__" RPCS3_QT_PLATFORM="__RPCS3_QT_PLATFORM__" -RPCS3_AUDIO_VOLUME="__RPCS3_AUDIO_VOLUME__" -RPCS3_PIPEWIRE_QUANTUM="__RPCS3_PIPEWIRE_QUANTUM__" RPCS3_MIC_SPLIT_MATCH="__RPCS3_MIC_SPLIT_MATCH__" RPCS3_MIC_VOLUME="__RPCS3_MIC_VOLUME__" RPCS3_MIC_SINGLE_MATCH="__RPCS3_MIC_SINGLE_MATCH__" @@ -966,21 +702,6 @@ fi start_audio() { start_kiosk_audio RPCS3 - - # WirePlumber recuerda un volumen bajo (40 %) en algunos equipos; se fija el - # volumen de la salida por defecto y se quita el silencio. - if [[ -n "$RPCS3_AUDIO_VOLUME" ]] && command -v wpctl >/dev/null 2>&1; then - wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null 2>&1 || true - wpctl set-volume @DEFAULT_AUDIO_SINK@ "$RPCS3_AUDIO_VOLUME" >/dev/null 2>&1 || true - fi - - # PipeWire trabaja por defecto con ciclos de 1024 muestras (21 ms a 48 kHz) y - # cada etapa de un microfono (dispositivo, fuente mono, lectura de RPCS3) puede - # sumar uno. Se fuerza un cuantum menor para bajar la latencia de los - # microfonos y del audio. Es de ejecucion, asi que se aplica en cada arranque. - if [[ -n "$RPCS3_PIPEWIRE_QUANTUM" ]] && command -v pw-metadata >/dev/null 2>&1; then - pw-metadata -n settings 0 clock.force-quantum "$RPCS3_PIPEWIRE_QUANTUM" >/dev/null 2>&1 || true - fi } find_rpcs3_bin() { @@ -1116,15 +837,6 @@ setup_pads() { SDL_VIDEODRIVER=dummy python3 "$script" 2>&1 | sed -u 's/^/[pads] /' >&2 || true } -# Atajo para cerrar RPCS3 desde el teclado (Ctrl+Alt+Q). -start_exit_hotkey() { - local script=/usr/local/bin/rpcs3-exit-hotkey.py - - [[ -f "$script" ]] && command -v python3 >/dev/null 2>&1 || return 0 - pgrep -u "$(id -u)" -f "$script" >/dev/null 2>&1 && return 0 - - python3 "$script" 2>&1 | sed 's/^/[exit-hotkey] /' >&2 & -} # Bateria electronica MIDI por USB (RPCS3 emula la bateria de Rock Band 3 a partir # de ella). RPCS3 guarda el dispositivo como "Drums" + nombre del puerto ALSA, @@ -1178,7 +890,7 @@ setup_midi_drums() { done < "$cfg" > "$tmp" && mv "$tmp" "$cfg" } -start_exit_hotkey +start_kiosk_exit_hotkey AppRun.wrapped rpcs3 start_audio while true; do @@ -1231,7 +943,7 @@ done TEMPLATE # El prologo (entorno, DBus, PipeWire) es comun a los tres kioscos. -RPCS3_WRAPPER_TEMPLATE="$(kiosk_wrapper_prelude run-rpcs3 __RPCS3_HOME__ false)" +RPCS3_WRAPPER_TEMPLATE="$(kiosk_wrapper_prelude run-rpcs3 __RPCS3_HOME__ false __RPCS3_AUDIO_VOLUME__ __RPCS3_PIPEWIRE_QUANTUM__)" RPCS3_WRAPPER_TEMPLATE+=$'\n\n'"$RPCS3_WRAPPER_BODY" install_rpcs3_update_script() { @@ -1246,21 +958,6 @@ install_rpcs3_update_script() { chmod +x /mnt/usr/local/bin/update-rpcs3 } -# Instala el atajo de salida (teclado Ctrl+Alt+Q). Cage no -# procesa Alt+F4 y RPCS3 sin GUI no tiene atajos para cerrarse, asi que un -# proceso aparte lee los dispositivos de entrada, como hace evmapy en Batocera. -install_rpcs3_exit_hotkey() { - if [[ "${RPCS3_EXIT_HOTKEY:-true}" != "true" ]]; then - log "Atajo de salida omitido (RPCS3_EXIT_HOTKEY=false)" - return 0 - fi - - log "Instalando atajo de salida de RPCS3 (Ctrl+Alt+Q)" - mkdir -p /mnt/usr/local/bin - printf '%s\n' "$RPCS3_EXIT_HOTKEY_TEMPLATE" > /mnt/usr/local/bin/rpcs3-exit-hotkey.py - chmod 755 /mnt/usr/local/bin/rpcs3-exit-hotkey.py -} - # Instala el script que asigna los controles conectados a los jugadores 1 y 2 # (rpcs3-pads.py); lo ejecuta el wrapper antes de cada arranque. Solo tiene # sentido con la configuracion de entrada SDL (RPCS3_PAD_CONFIG=true). diff --git a/lib/yarg.sh b/lib/yarg.sh index 498e6df..5bae491 100644 --- a/lib/yarg.sh +++ b/lib/yarg.sh @@ -137,6 +137,8 @@ install_yarg() { fi run_quiet arch-chroot /mnt find /opt/YARG -maxdepth 1 -type f -name 'YARG*' -exec chmod +x {} + + # Marca de la URL instalada: update-yarg no descarga si ya esta la ultima version. + printf '%s\n' "$YARG_URL" > /mnt/opt/YARG/.install-url ensure_yarg_songs_symlink || return 1 run_quiet arch-chroot /mnt chown -R "$KIOSK_USER:$KIOSK_USER" /opt/YARG run_quiet arch-chroot /mnt chown -h "$KIOSK_USER:$KIOSK_USER" /opt/YARG/Songs @@ -220,45 +222,6 @@ EOF fi } -configure_yarg_performance() { - log "Aplicando optimizaciones de rendimiento para YARG" - - mkdir -p /mnt/etc/security/limits.d /mnt/etc/sysctl.d /mnt/etc/default - - cat > /mnt/etc/security/limits.d/99-yarg.conf << EOF -$KIOSK_USER - rtprio 99 -$KIOSK_USER - memlock unlimited -$KIOSK_USER - nice -20 -EOF - - echo 'vm.swappiness=10' > /mnt/etc/sysctl.d/99-yarg.conf - - cat > /mnt/etc/default/cpupower << 'EOF' -# Versiones antiguas del servicio cpupower leen este archivo (en minusculas o -# mayusculas). Se escriben ambas formas. -GOVERNOR='performance' -MIN_FREQ='' -MAX_FREQ='' -governor='performance' -min_freq='' -max_freq='' -EOF - - # Los paquetes recientes de cpupower leen su configuracion de este otro - # archivo (EnvironmentFile de la unidad), no de /etc/default/cpupower: sin ese - # archivo el servicio termina con exito pero el gobernador queda en schedutil. - cat > /mnt/etc/default/cpupower-service.conf << 'EOF' -GOVERNOR='performance' -MIN_FREQ='' -MAX_FREQ='' -EOF - - if ! run_quiet arch-chroot /mnt systemctl enable cpupower.service; then - log_error "Fallo al habilitar cpupower.service" - return 1 - fi -} - install_yarg_update_script() { normalize_yarg_songs_dir @@ -279,6 +242,10 @@ INSTALL_DIR="/opt/YARG" SONGS_DIR="$YARG_SONGS_DIR" ZIP_FILE="/tmp/YARG_Linux.zip" OWNER="$KIOSK_USER" +# URL instalada; el nombre lleva la version. --force reinstala igual. +URL_MARK="\$INSTALL_DIR/.install-url" +FORCE=false +[[ "\${1:-}" == "--force" ]] && FORCE=true if [[ \${EUID} -ne 0 ]]; then echo "Este script debe ejecutarse como root." >&2 @@ -356,12 +323,19 @@ case "\$YARG_RELEASE_CHANNEL" in ;; esac +if [[ "\$FORCE" != "true" && -r "\$URL_MARK" && "\$(cat "\$URL_MARK")" == "\$YARG_URL" ]]; then + echo "YARG ya esta en la ultima version (\$(basename "\$YARG_URL")); no se descarga nada." + echo "Usa update-yarg --force para reinstalarla igual." + exit 0 +fi + echo "Descargando YARG desde: \$YARG_URL" curl -fsSL --retry 3 --retry-delay 2 -o "\$ZIP_FILE" "\$YARG_URL" unzip -tq "\$ZIP_FILE" >/dev/null unzip -o "\$ZIP_FILE" -d "\$INSTALL_DIR" >/dev/null ensure_songs_link find "\$INSTALL_DIR" -maxdepth 1 -type f -name "YARG*" -exec chmod +x {} + +printf '%s\n' "\$YARG_URL" > "\$URL_MARK" chown -R "\$OWNER:\$OWNER" "\$INSTALL_DIR" chown -h "\$OWNER:\$OWNER" "\$INSTALL_DIR/Songs" chown -R "\$OWNER:\$OWNER" "\$SONGS_DIR" diff --git a/tests/test_kiosk_runtime.bats b/tests/test_kiosk_runtime.bats index a7379b9..09cf492 100644 --- a/tests/test_kiosk_runtime.bats +++ b/tests/test_kiosk_runtime.bats @@ -86,6 +86,160 @@ stub() { # nombre cuerpo [[ "$output" == *"antes de iniciar Clone Hero."* ]] } +@test "start_kiosk_audio fija el volumen con wpctl cuando el prologo lo recibe" { + local calls="$BATS_TEST_TMPDIR/wpctl" + stub pgrep 'exit 0' + stub pactl 'echo "0 sink-falso"' + stub wpctl "echo \"\$*\" >> '$calls'" + { + kiosk_wrapper_prelude run-x /home/k false 0.8 + echo 'wait_for_path() { return 0; }' + echo 'start_kiosk_audio Prueba' + } > "$BATS_TEST_TMPDIR/a.sh" + + PATH="$STUBS:$PATH" XDG_RUNTIME_DIR="$BATS_TEST_TMPDIR" KIOSK_DBUS_SESSION_STARTED=1 \ + run bash "$BATS_TEST_TMPDIR/a.sh" + [ "$status" -eq 0 ] + grep -q 'set-mute @DEFAULT_AUDIO_SINK@ 0' "$calls" + grep -q 'set-volume @DEFAULT_AUDIO_SINK@ 0.8' "$calls" +} + +@test "start_kiosk_audio no toca el volumen si el prologo no recibe uno" { + local calls="$BATS_TEST_TMPDIR/wpctl" + stub pgrep 'exit 0' + stub pactl 'echo "0 sink-falso"' + stub wpctl "echo llamado >> '$calls'" + { + kiosk_wrapper_prelude run-x /home/k false + echo 'wait_for_path() { return 0; }' + echo 'start_kiosk_audio Prueba' + } > "$BATS_TEST_TMPDIR/a.sh" + + PATH="$STUBS:$PATH" XDG_RUNTIME_DIR="$BATS_TEST_TMPDIR" KIOSK_DBUS_SESSION_STARTED=1 \ + run bash "$BATS_TEST_TMPDIR/a.sh" + [ "$status" -eq 0 ] + [ ! -e "$calls" ] +} + +@test "validate_kiosk_audio_volume acepta de 0 a 1 o vacio y rechaza el resto" { + validate_kiosk_audio_volume V "" + validate_kiosk_audio_volume V 1.0 + validate_kiosk_audio_volume V 0.5 + run validate_kiosk_audio_volume V 1.5 + [ "$status" -eq 1 ] + run validate_kiosk_audio_volume V abc + [ "$status" -eq 1 ] +} + +@test "start_kiosk_audio fuerza el cuantum de PipeWire solo si el prologo recibe uno" { + local calls="$BATS_TEST_TMPDIR/pwmeta" + stub pgrep 'exit 0' + stub pactl 'echo "0 sink-falso"' + stub pw-metadata "echo \"\$*\" >> '$calls'" + { + kiosk_wrapper_prelude run-x /home/k false "" 128 + echo 'wait_for_path() { return 0; }' + echo 'start_kiosk_audio Prueba' + } > "$BATS_TEST_TMPDIR/a.sh" + PATH="$STUBS:$PATH" XDG_RUNTIME_DIR="$BATS_TEST_TMPDIR" KIOSK_DBUS_SESSION_STARTED=1 \ + run bash "$BATS_TEST_TMPDIR/a.sh" + [ "$status" -eq 0 ] + grep -q 'settings 0 clock.force-quantum 128' "$calls" + + rm -f "$calls" + { + kiosk_wrapper_prelude run-x /home/k false "" "" + echo 'wait_for_path() { return 0; }' + echo 'start_kiosk_audio Prueba' + } > "$BATS_TEST_TMPDIR/b.sh" + PATH="$STUBS:$PATH" XDG_RUNTIME_DIR="$BATS_TEST_TMPDIR" KIOSK_DBUS_SESSION_STARTED=1 \ + run bash "$BATS_TEST_TMPDIR/b.sh" + [ "$status" -eq 0 ] + [ ! -e "$calls" ] +} + +@test "validate_kiosk_pipewire_quantum acepta de 32 a 2048 o vacio y rechaza el resto" { + validate_kiosk_pipewire_quantum Q "" + validate_kiosk_pipewire_quantum Q 128 + validate_kiosk_pipewire_quantum Q 32 + validate_kiosk_pipewire_quantum Q 2048 + run validate_kiosk_pipewire_quantum Q 16 + [ "$status" -eq 1 ] + run validate_kiosk_pipewire_quantum Q 4096 + [ "$status" -eq 1 ] + run validate_kiosk_pipewire_quantum Q abc + [ "$status" -eq 1 ] +} + +# --- atajo de salida compartido ---------------------------------------------- + +@test "install_kiosk_exit_hotkey no instala nada con false" { + run install_kiosk_exit_hotkey false YARG + [ "$status" -eq 0 ] + [[ "$output" == *"omitido (YARG_EXIT_HOTKEY=false)"* ]] +} + +@test "start_kiosk_exit_hotkey pasa los procesos objetivo al script" { + local calls="$BATS_TEST_TMPDIR/hk" + stub pgrep 'exit 1' + stub python3 "echo \"\$*\" >> '$calls'" + { + kiosk_wrapper_prelude run-x /home/k false + echo 'wait_for_path() { return 0; }' + echo 'start_kiosk_exit_hotkey /opt/YARG/ proceso' + echo 'wait' + } > "$BATS_TEST_TMPDIR/a.sh" + # El script solo arranca si existe: se sustituye la ruta fija por una de prueba. + sed -i "s|/usr/local/bin/kiosk-exit-hotkey.py|$BATS_TEST_TMPDIR/kiosk-exit-hotkey.py|" "$BATS_TEST_TMPDIR/a.sh" + touch "$BATS_TEST_TMPDIR/kiosk-exit-hotkey.py" + + PATH="$STUBS:$PATH" XDG_RUNTIME_DIR="$BATS_TEST_TMPDIR" KIOSK_DBUS_SESSION_STARTED=1 \ + run bash "$BATS_TEST_TMPDIR/a.sh" + [ "$status" -eq 0 ] + grep -q "kiosk-exit-hotkey.py /opt/YARG/ proceso" "$calls" +} + +@test "el atajo de salida distingue rutas (-f con prefijo) de nombres exactos (-x)" { + command -v python3 >/dev/null || skip "python3 no esta instalado" + local script="$BATS_TEST_TMPDIR/kiosk-exit-hotkey.py" + printf '%s\n' "$KIOSK_EXIT_HOTKEY_TEMPLATE" > "$script" + + run python3 - "$script" <<'PY' +import importlib.util, sys +spec = importlib.util.spec_from_file_location("hotkey", sys.argv[1]) +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +assert m.match_args("/opt/YARG/") == ["-f", "^/opt/YARG/"] +assert m.match_args("rpcs3") == ["-x", "rpcs3"] +PY + [ "$status" -eq 0 ] +} + +# --- rendimiento y updaters compartidos ----------------------------------------- + +@test "las tres rutas usan configure_kiosk_performance y no quedan copias propias" { + grep -Fq 'configure_kiosk_performance YARG' install-cage-yarg.sh + grep -Fq 'configure_kiosk_performance "Clone Hero"' install-cage-clonehero.sh + grep -Fq 'configure_kiosk_performance RPCS3' install-cage-rpcs3.sh + run grep -rn 'configure_yarg_performance\|configure_clonehero_performance\|configure_rpcs3_performance' lib install-cage-*.sh + [ "$status" -ne 0 ] +} + +@test "update-yarg y update-clonehero no descargan si ya esta la ultima version y aceptan --force" { + for f in lib/yarg.sh lib/clonehero.sh; do + grep -Fq 'URL_MARK=' "$f" + grep -Fq '[[ "\${1:-}" == "--force" ]]' "$f" + grep -Fq '.install-url' "$f" + done +} + +@test "YARG y Clone Hero exigen un disco minimo configurable" { + grep -Fq 'check_disk "$DISK_DEVICE" "$YARG_MIN_DISK_GB"' install-cage-yarg.sh + grep -Fq 'check_disk "$DISK_DEVICE" "$CLONEHERO_MIN_DISK_GB"' install-cage-clonehero.sh + grep -q '^YARG_MIN_DISK_GB=' .env.example + grep -q '^CLONEHERO_MIN_DISK_GB=' .env.example +} + # --- menu de mantenimiento --------------------------------------------------- @test "el menu es Bash valido, sin marcadores, con titulo y subrayado del mismo largo" { diff --git a/tests/test_rpcs3.bats b/tests/test_rpcs3.bats index 32cd897..d7c0ce0 100644 --- a/tests/test_rpcs3.bats +++ b/tests/test_rpcs3.bats @@ -279,15 +279,15 @@ fake_appimage() { @test "el wrapper arranca el atajo de salida" { run render_wrapper "$BATS_TEST_TMPDIR/games" - [[ "$output" == *"start_exit_hotkey"* ]] - [[ "$output" == *"rpcs3-exit-hotkey.py"* ]] + [[ "$output" == *"start_kiosk_exit_hotkey AppRun.wrapped rpcs3"* ]] + [[ "$output" == *"kiosk-exit-hotkey.py"* ]] } @test "el script del atajo de salida compila y detecta las combinaciones" { command -v python3 >/dev/null || skip "python3 no esta instalado" - local script="$BATS_TEST_TMPDIR/rpcs3-exit-hotkey.py" - printf '%s\n' "$RPCS3_EXIT_HOTKEY_TEMPLATE" > "$script" + local script="$BATS_TEST_TMPDIR/kiosk-exit-hotkey.py" + printf '%s\n' "$KIOSK_EXIT_HOTKEY_TEMPLATE" > "$script" run python3 - "$script" <<'PY' import importlib.util, sys @@ -306,9 +306,8 @@ PY [ "$status" -eq 0 ] } -@test "install_rpcs3_exit_hotkey no instala nada con RPCS3_EXIT_HOTKEY=false" { - RPCS3_EXIT_HOTKEY=false - run install_rpcs3_exit_hotkey +@test "install_kiosk_exit_hotkey no instala nada con false" { + run install_kiosk_exit_hotkey false RPCS3 [ "$status" -eq 0 ] [[ "$output" == *"omitido"* ]] } @@ -629,12 +628,12 @@ run_updater_tail() { @test "el wrapper fuerza el cuantum de PipeWire con pw-metadata cuando RPCS3_PIPEWIRE_QUANTUM tiene valor" { run render_wrapper "$BATS_TEST_TMPDIR/games" - [[ "$output" == *'pw-metadata -n settings 0 clock.force-quantum "$RPCS3_PIPEWIRE_QUANTUM"'* ]] - [[ "$output" == *'[[ -n "$RPCS3_PIPEWIRE_QUANTUM" ]]'* ]] + [[ "$output" == *'pw-metadata -n settings 0 clock.force-quantum "$KIOSK_PIPEWIRE_QUANTUM"'* ]] + [[ "$output" == *'[[ -n "$KIOSK_PIPEWIRE_QUANTUM" ]]'* ]] } @test "el instalador valida RPCS3_PIPEWIRE_QUANTUM (32 a 2048) y lo documenta" { - run grep -n 'RPCS3_PIPEWIRE_QUANTUM invalido' install-cage-rpcs3.sh + run grep -n 'validate_kiosk_pipewire_quantum RPCS3_PIPEWIRE_QUANTUM' install-cage-rpcs3.sh [ "$status" -eq 0 ] grep -q '^RPCS3_PIPEWIRE_QUANTUM=' .env.example grep -q 'RPCS3_PIPEWIRE_QUANTUM' README.md @@ -757,10 +756,12 @@ PY @test "cada camino escribe la configuracion de cpupower tambien en cpupower-service.conf" { # La unidad cpupower de los paquetes recientes lee /etc/default/cpupower-service.conf; # sin ese archivo termina con exito pero el gobernador queda en schedutil. - for f in lib/rpcs3.sh lib/yarg.sh lib/clonehero.sh; do - run grep -c "/mnt/etc/default/cpupower-service.conf" "$f" - [ "$status" -eq 0 ] || { echo "$f no escribe cpupower-service.conf" >&2; return 1; } - done + # Hay una sola implementacion (configure_kiosk_performance) y las tres rutas la usan. + run grep -c "/mnt/etc/default/cpupower-service.conf" lib/kiosk_runtime.sh + [ "$status" -eq 0 ] + grep -Fq 'configure_kiosk_performance RPCS3' install-cage-rpcs3.sh + grep -Fq 'configure_kiosk_performance YARG' install-cage-yarg.sh + grep -Fq 'configure_kiosk_performance "Clone Hero"' install-cage-clonehero.sh } # --- escala de resolucion y override MIDI desde el instalador --------------------------------- @@ -805,12 +806,12 @@ PY # --- atajo de salida: escalar a SIGKILL cuando la GUI atrapa SIGTERM ----------------------- -# Ejecuta terminate_rpcs3() del script del atajo contra un proceso ficticio (una +# Ejecuta terminate_app() del script del atajo contra un proceso ficticio (una # copia de python3 con otro nombre, para que pkill -x no toque nada real). run_terminate_dummy() { local ignore_term="$1" grace="$2" name="dummyrpcs3$$" local script="$BATS_TEST_TMPDIR/hotkey.py" - printf '%s\n' "$RPCS3_EXIT_HOTKEY_TEMPLATE" > "$script" + printf '%s\n' "$KIOSK_EXIT_HOTKEY_TEMPLATE" > "$script" cp "$(command -v python3)" "$BATS_TEST_TMPDIR/$name" run python3 - "$script" "$BATS_TEST_TMPDIR/$name" "$name" "$ignore_term" "$grace" <<'PY' @@ -821,7 +822,7 @@ code = "import signal,time\n" + ("signal.signal(signal.SIGTERM, signal.SIG_IGN)\ proc = subprocess.Popen([binary, "-c", code]) time.sleep(0.5) t0 = time.monotonic() -terminate_rpcs3("prueba", targets=(name,), grace=float(grace)) +terminate_app("prueba", (name,), grace=float(grace)) elapsed = time.monotonic() - t0 alive = running((name,)) proc.kill(); proc.wait()