fix: end handheld runs that lose their input

Start + Select now kills a sentinel the launcher owns, and the launcher
asks the interface to stop, so a running install still writes its result.
A mapper that dies, a chvt the compositor refuses, a terminal app that
cannot open and a read-only card each end the run or hand over to the
next route instead of a console waiting for a button. muOS starts
gptokeyb2 with -X and keeps muxterm off the pad so Y opens the options.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 18:21:45 +02:00
1 parent bfd82cedf8
commit 5db889f538
11 files changed
+1228 -205

No files matched your search

+24 -6
View File
@@ -28,8 +28,10 @@ GAMEDIR="/$directory/ports/retrobios"
cd "$GAMEDIR" || exit 1
# Appended to, as the launcher and install.py also write this file: a tee
# writing from its own offset would overwrite their lines.
: > "$GAMEDIR/log.txt" && exec > >(tee -a "$GAMEDIR/log.txt") 2>&1
# writing from its own offset would overwrite their lines. The previous run's
# is kept, for a player who starts again before reading it.
[ -s "$GAMEDIR/log.txt" ] && mv -f "$GAMEDIR/log.txt" "$GAMEDIR/log.prev.txt" 2>/dev/null
{ : > "$GAMEDIR/log.txt"; } 2>/dev/null && exec > >(tee -a "$GAMEDIR/log.txt") 2>&1
# PortMasterDialog.txt has shipped with PortMaster since before 2024.02.26,
# and the 8.5.23 copy JELOS and KOLOS preinstall has no funcs.txt to source
@@ -48,17 +50,33 @@ RB_CONSOLE="${CUR_TTY:-/dev/tty0}"
RB_MAPPER="$GPTOKEYB"
RB_FALLBACK=1
RB_PACK_PATH="$GAMEDIR:/$directory/ports:/$directory"
# ArkOS and dArkOS, as control.txt tells them apart (its plymouth theme): the
# games card is /$directory, roms2 when fstab mounts a second card, and its
# bios folder is the one RetroArch reads. The other card's is offered.
# ArkOS and dArkOS, as control.txt tells them apart (its plymouth theme):
# their two card folders are offered to each other, and the destination is
# the one RetroArch reads. control.txt names the card PortMaster runs from
# roms2 whenever fstab mounts a second card, even when RetroArch still reads
# /roms/bios, so RetroArch's own setting decides, as in the Tools entry.
if [ -e "${RB_ARK_MARKER:-/usr/share/plymouth/themes/text.plymouth}" ]; then
RB_OTHER_CARDS="${RB_OTHER_CARDS:-/roms/bios:/roms2/bios}"
RB_BIOS_DEST="/$directory/bios"
RB_BIOS_DEST=$(awk -F'"' '/^system_directory *=/ { print $2; exit }' \
"${RB_RETROARCH_CFG:-/home/ark/.config/retroarch/retroarch.cfg}" 2>/dev/null)
RB_BIOS_DEST="${RB_BIOS_DEST:-/$directory/bios}"
fi
# shellcheck source=../shared/tui.txt
source "$GAMEDIR/tui.txt"
# shellcheck source=retrobios/screens.txt
source "$GAMEDIR/screens.txt"
rb_open_log keep
# muOS: PortMaster's mod_muOS.txt points CUR_TTY at /dev/null, so there is no
# console to draw on. muxterm, the terminal muOS's Terminal core runs, shows
# the interface instead. It is told to leave the pad alone (an SDL hint) so
# that gptokeyb sends every button once, with this port's mapping: muxterm
# sends Y as Space, where the interface reads Y as Options. The hint allows
# one vendor and product id, 0001:0001, which no pad carries.
RB_MUXTERM="${RB_MUXTERM:-/opt/muos/frontend/muxterm}"
if [ -x "$RB_MUXTERM" ] && [ -n "$RB_MAPPER" ]; then
RB_TERMAPP=(env SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=0x0001/0x0001 "$RB_MUXTERM" --gl --)
fi
export SDL_GAMECONTROLLERCONFIG="$sdl_controllerconfig"
+3 -2
View File
@@ -17,7 +17,8 @@ options, L1 and R1 to page. B on the first screen closes it.
EmulationStation covers the text console on Batocera and Knulli, and no
terminal for a script is documented there. RetroBIOS opens xterm (the X11
builds) or foot when either is installed, else switches to console 4, runs
there, and switches back. Which of the two shows up on a given build is not
verified. If the screen stays black, `retrobios/log.txt` says which route
there, and switches back. When console 4 does not come to the front within
3 s, it switches back and stops. Which of the two shows up on a given build
is not verified. If nothing shows, `retrobios/log.txt` says which route
was taken, and the computer route always works: with the card mounted, run
the one-line command from abdess.github.io/retrobios/wiki/installer.
+9 -3
View File
@@ -18,14 +18,20 @@ RB_PACK_PATH="$RB_DIR:$HERE:/userdata"
RB_LOG="$RB_DIR/log.txt"
# The X11 builds start EmulationStation on display :0, which a script run
# from it does not always inherit.
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] && [ -S /tmp/.X11-unix/X0 ]; then
# from it does not always inherit, with the cookie S31emulationstation
# exports: xterm is refused the display without it.
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] \
&& [ -S "${RB_X_SOCKET:-/tmp/.X11-unix/X0}" ]; then
export DISPLAY=:0
fi
RB_XAUTH="${RB_XAUTH:-/var/lib/.Xauthority}"
if [ -n "${DISPLAY:-}" ] && [ -z "${XAUTHORITY:-}" ] && [ -f "$RB_XAUTH" ]; then
export XAUTHORITY="$RB_XAUTH"
fi
# shellcheck source=../shared/tui.txt
source "$RB_DIR/tui.txt"
: > "$RB_LOG"
rb_open_log fresh
rb_select_python
# A launcher's own arguments are not install.py's (muOS passes the app folder).
# shellcheck disable=SC2119
+8 -6
View File
@@ -7,10 +7,12 @@ It checks the BIOS folder (`/run/muos/storage/bios`) against the files its
emulators read and installs the missing ones: downloaded with a network
connection, or from a pack copied to the root of the card without one.
It runs in muxterm, muOS's terminal. muxterm already sends the D-pad as
arrows, Start as Enter, B as Backspace (back) and Y as Space (tick). With
PortMaster installed, gptokeyb2 adds A (choose), X (tick) and L1/R1 (page).
Menu closes it.
It runs in muxterm, muOS's terminal. With PortMaster installed, gptokeyb2
turns every button into a key and muxterm is told to leave the pad alone, so
each button is sent once: D-pad to move, A or Start to choose, B or Select to
go back, X to tick a system, Y for the options, L1 and R1 to page. Hold
Select, then press Start to quit. Without PortMaster, muxterm reads the pad
itself (Y then ticks a system) and Menu closes it.
Not tried on a device yet: the icon, and whether muxterm and gptokeyb2 share
the pad as described.
Not tried on a device yet: the icon, and whether muxterm honours SDL's
controller allow-list as SDL 2.30 does.
+20 -9
View File
@@ -5,8 +5,8 @@
# RetroBIOS for muOS, from Applications: the installer's full-screen
# interface in muxterm, the terminal muOS's own Terminal core runs, with
# PortMaster's gptokeyb2 adding the keys muxterm leaves unmapped. Without
# muxterm it runs on the console with the full mapping.
# PortMaster's gptokeyb2 turning the buttons into keys. Without muxterm it
# runs on the console.
MUOS="${RB_MUOS:-/opt/muos}"
# shellcheck source=/dev/null
@@ -19,12 +19,12 @@ PM_DIR="$ROM_MOUNT/MUOS/PortMaster"
MUXTERM="$MUOS/frontend/muxterm"
RB_DIR="$APP_DIR"
RB_PACK_PATH="$APP_DIR:$ROM_MOUNT"
RB_LOG="$APP_DIR/log.txt"
# muOS binds MUOS/bios from SD2 when that card has one, else from SD1, onto
# /run/muos/storage/bios (script/device/bind.sh:10-11,30-52): the other card's
# MUOS/bios is the one offered to copy from.
SD2_MOUNT="$(GET_VAR "device" "storage/sdcard/mount")"
RB_PACK_PATH="$APP_DIR:$ROM_MOUNT:$SD2_MOUNT"
RB_OTHER_CARDS="$ROM_MOUNT/MUOS/bios:$SD2_MOUNT/MUOS/bios"
RB_BIOS_DEST="${RB_BIOS_DEST:-/run/muos/storage/bios}"
# PortMaster keeps its runtimes here: the Python runtime is used when
@@ -33,21 +33,32 @@ controlfolder="$PM_DIR"
# shellcheck source=../shared/tui.txt
source "$APP_DIR/tui.txt"
: > "$RB_LOG"
rb_open_log fresh
RB_GPTK="$APP_DIR/retrobios.gptk"
if [ -x "$PM_DIR/gptokeyb2" ]; then
# As muOS's own launchers start it (script/launch/ext-xroar.sh).
# As muOS's own launchers start it (script/launch/ext-xroar.sh). gptokeyb2
# kills only with SIGKILL: by process name by default, which never names
# this run, and quits all the same, or with -X by command line, which is
# what reaches the Start + Select sentinel (gptokeyb2 src/util.c).
ln -sf "$PM_DIR/libinterpose.aarch64.so" "/usr/lib/libinterpose.aarch64.so" >/dev/null 2>&1
RB_MAPPER="$PM_DIR/gptokeyb2"
RB_MAPPER="$PM_DIR/gptokeyb2 -X"
fi
if [ -x "$MUXTERM" ]; then
RB_ROUTES="app"
RB_TERMAPP=("$MUXTERM" --gl --)
RB_GPTK="$APP_DIR/muxterm.gptk"
if [ -n "${RB_MAPPER:-}" ]; then
# muxterm reads the pad itself and sends Y as Space (MustardOS/terminal
# input.c), where the interface reads Y as Options. The SDL hint allows
# only vendor and product 0001:0001, which no pad carries, so muxterm
# leaves the pad to gptokeyb2 and every button is sent once.
RB_TERMAPP=(env SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT=0x0001/0x0001 "$MUXTERM" --gl --)
else
# No gptokeyb2: muxterm's own buttons are all there is.
RB_TERMAPP=("$MUXTERM" --gl --)
fi
else
RB_ROUTES="vt"
RB_CONSOLE="${RB_CONSOLE:-/dev/tty0}"
RB_GPTK="$APP_DIR/retrobios.gptk"
fi
rb_select_python
-10
View File
@@ -1,10 +0,0 @@
# RetroBIOS inside muxterm (gptokeyb2). muxterm already sends the D-pad as
# arrows, Start as Enter, B as Backspace and Y as Space to the program it
# runs (MustardOS/terminal input.c and osk.c), so mapping those here too
# would press each key twice. Only what muxterm leaves unmapped is added.
[controls]
overlay = clear
a = enter
x = space
l1 = pageup
r1 = pagedown
+434 -95
View File
@@ -5,18 +5,23 @@
#
# Sourced by every launcher (the PortMaster port, the Batocera and Knulli
# port, the muOS app, the Tools entry), each of which sets the inputs below
# and calls rb_select_python, then rb_launch. Run as `bash tui.txt here`, it
# is the part that runs inside a terminal app the launcher opened.
# and calls rb_open_log, rb_select_python, then rb_launch. Run as
# `bash tui.txt here`, it is the part that runs inside a terminal app the
# launcher opened.
#
# Inputs, all optional:
# RB_ROUTES where to look for a terminal, in order: here (this
# script's own stdin and stdout), app (foot or xterm in the
# graphical session), vt (a Linux console). Default "app vt".
# A route that fails to show the interface hands over to the
# next one.
# RB_CONSOLE the console of the vt route (default /dev/tty0)
# RB_VT_SWITCH a console number to switch to when a graphical session
# covers the current one (Batocera, Knulli)
# RB_MAPPER the command that starts gptokeyb, kill flag included
# ("$GPTOKEYB" in a PortMaster port), empty for none
# RB_MAPPER_NEEDED 0 when the terminal reads the pad itself, so the
# interface still has buttons without the mapper
# RB_GPTK its mapping (default retrobios.gptk next to this file)
# RB_PACK_PATH folders searched for a pack when there is no network,
# separated by ":", the first one named to the player
@@ -44,33 +49,140 @@ RB_FB="${RB_FB:-/sys/class/graphics/fb0/virtual_size}"
RB_ROUTE_TABLE="${RB_ROUTE_TABLE:-/proc/net/route}"
RB_VT_ACTIVE="${RB_VT_ACTIVE:-/sys/class/tty/tty0/active}"
RB_PROBE_URL="${RETROBIOS_BASE_URL:-https://raw.githubusercontent.com/Abdess/retrobios/}"
# Seconds: the interface's time to write its result once asked to stop, the
# wait for a button when the mapper is gone, the offline detection run.
RB_STOP_GRACE="${RB_STOP_GRACE:-10}"
RB_DEAD_WAIT="${RB_DEAD_WAIT:-10}"
RB_DETECT_LIMIT="${RB_DETECT_LIMIT:-20}"
RB_MAPPER_NEEDED="${RB_MAPPER_NEEDED:-1}"
RB_RUNTIME="python_3.11"
RB_RELEASES="github.com/Abdess/retrobios/releases/latest"
RB_PC_ROUTE="On a computer, with this SD card mounted, the same installer fills the card: install.py --platform NAME --dest BIOS-FOLDER-ON-THE-CARD. Instructions: abdess.github.io/retrobios/wiki/installer"
RB_PYTHON="${RB_PYTHON:-}"
RB_PYTHONHOME="${RB_PYTHONHOME:-}"
RB_PYTHON_PROBLEM="${RB_PYTHON_PROBLEM:-}"
RB_MAPPER_PID="${RB_MAPPER_PID:-}"
RB_STOP_PID="${RB_STOP_PID:-}"
RB_STOP_TOKEN="${RB_STOP_TOKEN:-}"
RB_STATE_PROBLEM=""
RB_MOUNTED=""
RB_MAPPER_PID=""
RB_UI_PID=""
RB_APP_PID=""
RB_STOPPED=""
RB_TTY=""
RB_STTY=""
RB_FONT_BACK=""
RB_VT_BACK=""
RB_ROUTE=""
RB_ROUTE_FAILED=""
RB_NAP_FD=""
RB_KEY=""
RB_TERMAPP=()
RB_PYCMD=()
rb_arch() {
uname -m
}
rb_py() {
rb_say() {
printf '%s\n' "RetroBIOS: $*" >> "$RB_LOG" 2>/dev/null
}
# The interpreter as one command, so it can be started in the background and
# signalled itself: a function run with & is a subshell whose python child a
# kill does not reach.
rb_pycmd() {
if [ -n "$RB_PYTHONHOME" ]; then
PYTHONHOME="$RB_PYTHONHOME" "$RB_PYTHON" "$@"
RB_PYCMD=(env "PYTHONHOME=$RB_PYTHONHOME" "$RB_PYTHON")
else
"$RB_PYTHON" "$@"
RB_PYCMD=("$RB_PYTHON")
fi
}
rb_py() {
rb_pycmd
"${RB_PYCMD[@]}" "$@"
}
# A pause without a child process, in fractions of a second, which busybox
# sleep does not always take: a read that times out on a pipe this shell
# holds both ends of.
rb_nap() {
if [ -z "$RB_NAP_FD" ]; then
exec {RB_NAP_FD}<> <(:) || RB_NAP_FD="none"
fi
if [ "$RB_NAP_FD" = "none" ]; then
sleep 1
else
read -r -t "$1" -u "$RB_NAP_FD" _
fi
return 0
}
# Whether a process runs. /proc, not kill -0: the mapper may run as root
# under sudo, and kill -0 on it fails for a user. A zombie is not running.
rb_alive() {
local stat
[ -n "${1:-}" ] || return 1
{ read -r stat < "/proc/$1/stat"; } 2>/dev/null || return 1
stat="${stat##*) }"
[ "${stat%% *}" != "Z" ]
}
# Runs a command for at most $1 seconds: status 124 when it was stopped.
rb_bounded() {
local limit="$1" pid waited=0
shift
"$@" &
pid=$!
while rb_alive "$pid"; do
if [ "$waited" -ge $((limit * 4)) ]; then
kill "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
return 124
fi
rb_nap 0.25
waited=$((waited + 1))
done
wait "$pid"
}
# --- State and log --------------------------------------------------------
rb_can_write() {
mkdir -p "$1" 2>/dev/null || return 1
{ printf 'x' > "$1/.probe"; } 2>/dev/null || return 1
rm -f "$1/.probe"
}
# The folder the run keeps its state in, and its log, started afresh with the
# previous one kept as log.prev.txt. A read-only or full card moves both to
# XDG_RUNTIME_DIR or /tmp: the interface then runs and says itself what it
# cannot write. "fresh" starts the log, "keep" leaves it as the launcher
# opened it.
rb_open_log() {
local spare="${XDG_RUNTIME_DIR:-/tmp}/retrobios" moved=""
if ! rb_can_write "$RB_STATE" || ! { : >> "$RB_LOG"; } 2>/dev/null; then
moved="$RB_DIR"
RB_STATE="$spare/run"
RB_LOG="$spare/log.txt"
if ! rb_can_write "$RB_STATE"; then
RB_STATE_PROBLEM="RetroBIOS cannot write to $moved or to $spare: the SD card may be read-only or full."
return 1
fi
fi
if [ "${1:-}" != "keep" ] || [ -n "$moved" ]; then
if [ -s "$RB_LOG" ]; then
mv -f "$RB_LOG" "${RB_LOG%.txt}.prev.txt" 2>/dev/null
fi
: > "$RB_LOG"
fi
if [ -n "$moved" ]; then
rb_say "$moved cannot be written (read-only or full card): the log and the run's files are in $spare"
fi
return 0
}
# --- Python ---------------------------------------------------------------
rb_python_ok() {
@@ -144,20 +256,22 @@ rb_python_text() {
# Whether the file list can be reached. Only a failure to resolve or to
# connect counts as offline: a certificate refused by an old curl still means
# a network, and install.py names that problem itself.
# a network, and install.py names that problem itself. Bounded as a whole:
# GNU wget retries a timeout 20 times.
rb_online() {
local code
if command -v curl >/dev/null 2>&1; then
curl -sSI --max-time 8 "$RB_PROBE_URL" >/dev/null 2>&1
rb_bounded 8 curl -sSI --connect-timeout 5 --max-time 7 "$RB_PROBE_URL" >/dev/null 2>&1
code=$?
case "$code" in
5|6|7|28) return 1 ;;
5|6|7|28|124) return 1 ;;
*) return 0 ;;
esac
fi
if command -v wget >/dev/null 2>&1; then
wget -q --spider -T 8 "$RB_PROBE_URL" >/dev/null 2>&1
[ $? -ne 4 ]
rb_bounded 8 wget -q --spider -T 7 "$RB_PROBE_URL" >/dev/null 2>&1
code=$?
[ "$code" -ne 4 ] && [ "$code" -ne 124 ]
return
fi
# Neither tool: a default route is the best guess there is.
@@ -166,12 +280,18 @@ rb_online() {
# The system and BIOS folder install.py finds here, read without a network:
# detection is local, and the run stops where the file list would download.
# That download is sent to a loopback port nothing listens on, so it fails
# at once instead of retrying three times 30 s on a network already found
# missing.
rb_detect() {
RB_PLATFORM=""
RB_PLATFORM_NAME=""
RB_BIOS_DIR=""
mkdir -p "$RB_STATE"
rb_py "$RB_INSTALL" --check --events > "$RB_STATE/detect.jsonl" 2>> "$RB_LOG"
rb_pycmd
RETROBIOS_BASE_URL="http://127.0.0.1:9" rb_bounded "$RB_DETECT_LIMIT" \
"${RB_PYCMD[@]}" "$RB_INSTALL" --check --events > "$RB_STATE/detect.jsonl" 2>> "$RB_LOG"
[ $? = 124 ] && rb_say "the detection run took over $RB_DETECT_LIMIT s and was stopped"
local kind a b c
while IFS=$'\t' read -r kind a b c; do
if [ "$kind" = "platform" ]; then
@@ -269,44 +389,50 @@ rb_terminal_app() {
fi
}
# Sets RB_ROUTE to the first of RB_ROUTES that can show text: here, app,
# vt, or none.
rb_route() {
local route
RB_ROUTE="none"
for route in $RB_ROUTES; do
case "$route" in
here)
if [ -t 0 ] && [ -t 1 ]; then
RB_ROUTE="here"
return 0
fi
;;
app)
if rb_terminal_app; then
RB_ROUTE="app"
return 0
fi
;;
vt)
# A compositor or X server holds the console it runs on: text
# written there is never seen, so the console needs a switch.
if rb_graphical; then
if [ -n "${RB_VT_SWITCH:-}" ] && command -v chvt >/dev/null 2>&1 \
&& [ -n "$(rb_active_vt)" ]; then
RB_ROUTE="vt"
return 0
fi
elif [ -c "$RB_CONSOLE" ]; then
RB_ROUTE="vt"
return 0
fi
;;
esac
done
# A console a terminal can be read from. PortMaster's mod_muOS.txt sets
# CUR_TTY to /dev/null, a character device that is no console at all. A
# console this user cannot open yet (rb_vt_enter opens it up) is judged by
# its name.
rb_is_console() {
[ -c "$1" ] || return 1
if [ -r "$1" ] && [ -w "$1" ]; then
{ [ -t 0 ]; } < "$1" 2>/dev/null
return
fi
case "$1" in
/dev/tty[0-9]*|/dev/console|/dev/pts/*) return 0 ;;
esac
return 1
}
# Whether a route can show text here. A compositor or X server holds the
# console it runs on: text written there is never seen, so the console needs
# a switch.
rb_route_ready() {
case "$1" in
here)
[ -t 0 ] && [ -t 1 ]
;;
app)
rb_terminal_app
;;
vt)
if rb_graphical; then
[ -n "${RB_VT_SWITCH:-}" ] && command -v chvt >/dev/null 2>&1 \
&& [ -n "$(rb_active_vt)" ]
elif rb_is_console "$RB_CONSOLE"; then
return 0
else
rb_say "$RB_CONSOLE is not a console: no route on it"
return 1
fi
;;
*)
return 1
;;
esac
}
# The largest Terminus font that keeps 56 columns and 20 rows, the way
# dArkOS's own tools pick one per screen (Lat7-Terminus16 on 480x320,
# TerminusBold22x11 on 640x480, 32x16 on 1024x768). Only changed when the
@@ -368,12 +494,34 @@ rb_active_vt() {
printf '%s\n' "$active"
}
# chvt, and whether the console really came to the front. chvt waits for the
# switch (VT_WAITACTIVE): a compositor that keeps the display would hold it
# forever, so it gets 3 s.
rb_chvt() {
local pid waited=0
$ESUDO chvt "$1" >> "$RB_LOG" 2>&1 &
pid=$!
while [ "$waited" -lt 12 ]; do
[ "$(rb_active_vt)" = "$1" ] && return 0
rb_nap 0.25
waited=$((waited + 1))
done
$ESUDO kill "$pid" >/dev/null 2>&1
return 1
}
rb_vt_enter() {
RB_TTY="$RB_CONSOLE"
if rb_graphical && [ -n "${RB_VT_SWITCH:-}" ]; then
RB_VT_BACK=$(rb_active_vt)
if ! rb_chvt "$RB_VT_SWITCH"; then
rb_say "console $RB_VT_SWITCH did not come to the front (console ${RB_VT_BACK:-?} still is)"
[ -n "$RB_VT_BACK" ] && rb_chvt "$RB_VT_BACK"
RB_TTY=""
RB_VT_BACK=""
return 1
fi
RB_TTY="${RB_VT_PREFIX:-/dev/tty}$RB_VT_SWITCH"
$ESUDO chvt "$RB_VT_SWITCH" >> "$RB_LOG" 2>&1
fi
if [ ! -r "$RB_TTY" ] || [ ! -w "$RB_TTY" ]; then
$ESUDO chmod 666 "$RB_TTY" >> "$RB_LOG" 2>&1
@@ -391,32 +539,98 @@ rb_vt_leave() {
stty "$RB_STTY" < "$RB_TTY" 2>/dev/null
fi
if [ -n "$RB_VT_BACK" ]; then
$ESUDO chvt "$RB_VT_BACK" >> "$RB_LOG" 2>&1
rb_chvt "$RB_VT_BACK"
fi
RB_TTY=""
RB_STTY=""
RB_VT_BACK=""
}
# gptokeyb on a name only this installer's processes carry: kill mode runs
# pkill -f on it, and the path of install.py is in the command line of the
# interface and of every run it starts, never in python3's alone.
# --- Controller and Start + Select ----------------------------------------
# Start + Select is the mapper's kill mode: gptokeyb runs pkill -f on its
# target, then pkill -9 3 s later, and gptokeyb2 -X runs kill -9 at once.
# Either would reach the interface's engine too, whose command line carries
# the installer's path, and SIGKILL leaves no time to say what an install
# wrote. So the target is a sentinel, a process this launcher owns whose
# command line alone carries the token: when it dies, the launcher asks the
# interface to stop, and the interface gives a running install its 2 s.
rb_stop_start() {
RB_STOP_TOKEN="retrobios-stop-${$}x"
bash -c 'exec {f}<> <(:); while :; do read -r -t 3600 -u "$f" _; done' \
"$RB_STOP_TOKEN" > /dev/null 2>&1 &
RB_STOP_PID=$!
}
rb_stop_requested() {
[ -n "$RB_STOP_PID" ] && ! rb_alive "$RB_STOP_PID"
}
rb_stop_end() {
[ -n "$RB_STOP_PID" ] || return 0
kill "$RB_STOP_PID" >/dev/null 2>&1
RB_STOP_PID=""
}
# The mapper, checked half a second after it starts: where it is the only
# input, a mapper that exits at once leaves an interface nothing can move.
rb_mapper_start() {
[ -n "${RB_MAPPER:-}" ] || return 0
[ -n "$RB_MAPPER_PID" ] && return 0
rb_stop_start
# shellcheck disable=SC2086 # RB_MAPPER is a command and its flags, as $GPTOKEYB is
$RB_MAPPER "$RB_INSTALL" -c "$RB_GPTK" >> "$RB_LOG" 2>&1 &
$RB_MAPPER "$RB_STOP_TOKEN" -c "$RB_GPTK" >> "$RB_LOG" 2>&1 &
RB_MAPPER_PID=$!
rb_nap 0.5
if ! rb_alive "$RB_MAPPER_PID"; then
rb_say "the controller mapper ($RB_MAPPER) stopped as soon as it started"
fi
}
# The mapper was started, is needed, and is no longer running.
rb_mapper_gone() {
[ -n "${RB_MAPPER:-}" ] && [ -n "$RB_MAPPER_PID" ] && [ "$RB_MAPPER_NEEDED" != "0" ] \
&& ! rb_alive "$RB_MAPPER_PID"
}
rb_mapper_stop() {
[ -n "$RB_MAPPER_PID" ] || return 0
$ESUDO kill "$RB_MAPPER_PID" >/dev/null 2>&1
wait "$RB_MAPPER_PID" 2>/dev/null
if [ -n "$RB_MAPPER_PID" ]; then
$ESUDO kill "$RB_MAPPER_PID" >/dev/null 2>&1
wait "$RB_MAPPER_PID" 2>/dev/null
fi
RB_MAPPER_PID=""
rb_stop_end
}
# A screen of text on the current terminal, closed by any button.
rb_message() {
rb_mapper_text() {
printf '%s\n' "The controller cannot be read: gptokeyb, which turns the buttons into keys, stopped as soon as it started. What it said is in $RB_LOG. $RB_PC_ROUTE"
}
# --- Messages -------------------------------------------------------------
# One key into RB_KEY. False when none came: the wait ran out, Start + Select
# was pressed, or the mapper is gone and only a keyboard is left, which gets
# RB_DEAD_WAIT seconds.
rb_read_key() {
local limit="${RB_MESSAGE_WAIT:-300}" waited=0 code
RB_KEY=""
while [ "$waited" -lt "$limit" ]; do
rb_stop_requested && return 1
if rb_mapper_gone && [ "$limit" -gt "$RB_DEAD_WAIT" ]; then
limit="$RB_DEAD_WAIT"
[ "$waited" -lt "$limit" ] || return 1
fi
IFS= read -r -s -n 1 -t 1 RB_KEY
code=$?
[ "$code" = 0 ] && return 0
# The end of the input, not a timeout.
[ "$code" -gt 128 ] || return 1
waited=$((waited + 1))
done
return 1
}
rb_screen() {
local cols
cols=$(stty size 2>/dev/null | awk '{print $2}')
case "$cols" in
@@ -426,53 +640,116 @@ rb_message() {
printf '\033[2J\033[H'
printf 'RetroBIOS\n\n'
printf '%s\n' "$1" | fold -s -w $((cols - 1))
printf '\n%s\n' "Press A or Enter to close."
read -r -s -n 1 -t "${RB_MESSAGE_WAIT:-300}" _
printf '\n%s\n' "$2"
}
# A screen of text on the current terminal, closed by any button.
rb_message() {
rb_screen "$1" "Press A or Enter to close."
rb_read_key
printf '\033[2J\033[H'
}
# A question on the current terminal: true for A or Enter, false for B, Esc,
# any other button or no answer.
rb_ask() {
local cols key
cols=$(stty size 2>/dev/null | awk '{print $2}')
case "$cols" in
''|*[!0-9]*) cols=40 ;;
esac
[ "$cols" -ge 20 ] || cols=40
printf '\033[2J\033[H'
printf 'RetroBIOS\n\n'
printf '%s\n' "$1" | fold -s -w $((cols - 1))
printf '\n%s\n' "A or Enter: yes. B or Esc: no."
if ! IFS= read -r -s -n 1 -t "${RB_MESSAGE_WAIT:-300}" key; then
rb_screen "$1" "A or Enter: yes. B or Esc: no."
if ! rb_read_key; then
printf '\033[2J\033[H'
return 1
fi
# The rest of an escape sequence (an arrow) is not a second answer.
read -r -s -t 0.05 -n 8 _ 2>/dev/null
printf '\033[2J\033[H'
[ -z "$key" ] || [ "$key" = " " ]
[ -z "$RB_KEY" ] || [ "$RB_KEY" = " " ]
}
# --- The run --------------------------------------------------------------
# The interface itself, watched: Start + Select or a mapper gone (nothing
# moves it any more) asks it to stop with SIGTERM, then SIGKILL after
# RB_STOP_GRACE seconds. Its stdin is named, since a background command in a
# script reads /dev/null otherwise.
rb_interface() {
local status naps=0
rb_pycmd
# A Ctrl-C typed while the terminal still sends signals reaches the
# interface on its own: this shell keeps watching.
trap ':' INT
"${RB_PYCMD[@]}" "$RB_INSTALL" --tui "$@" <&0 2> "$RB_STATE/tui.err" &
RB_UI_PID=$!
while rb_alive "$RB_UI_PID"; do
if [ -z "$RB_STOPPED" ]; then
if rb_stop_requested; then
RB_STOPPED="start_select"
rb_say "Start + Select: the interface is asked to stop"
elif rb_mapper_gone; then
RB_STOPPED="mapper"
rb_say "the controller mapper stopped: the interface is asked to stop"
fi
[ -n "$RB_STOPPED" ] && kill -TERM "$RB_UI_PID" 2>/dev/null
else
naps=$((naps + 1))
if [ "$naps" -ge $((RB_STOP_GRACE * 4)) ]; then
rb_say "the interface did not stop within $RB_STOP_GRACE s"
kill -KILL "$RB_UI_PID" 2>/dev/null
rb_kill_engines
fi
fi
rb_nap 0.25
done
wait "$RB_UI_PID"
status=$?
trap - INT
RB_UI_PID=""
return "$status"
}
# An engine the interface started survives it when the interface is killed:
# it runs in its own session.
rb_kill_engines() {
command -v pkill >/dev/null 2>&1 || return 0
pkill -KILL -f "$RB_INSTALL" >/dev/null 2>&1
}
rb_ui_stop() {
local naps=0
rb_alive "$RB_UI_PID" || return 0
kill -TERM "$RB_UI_PID" 2>/dev/null
while rb_alive "$RB_UI_PID" && [ "$naps" -lt $((RB_STOP_GRACE * 4)) ]; do
rb_nap 0.25
naps=$((naps + 1))
done
if rb_alive "$RB_UI_PID"; then
kill -KILL "$RB_UI_PID" 2>/dev/null
rb_kill_engines
fi
RB_UI_PID=""
}
# The interface on the current terminal: install.py --tui, from a pack on the
# card when there is no network. The status also goes to RB_STATE/status,
# since a terminal app does not always pass its child's on.
rb_here() {
local status pack args=()
local status pack args=() other
mkdir -p "$RB_STATE"
rm -f "$RB_STATE/status"
case "${TERM:-}" in
''|dumb|unknown) export TERM=xterm ;;
esac
if rb_mapper_gone; then
rb_message "$(rb_mapper_text)"
printf '1\n' > "$RB_STATE/status"
return 1
fi
if [ -z "$RB_PYTHON" ]; then
rb_message "$(rb_python_text)"
printf '1\n' > "$RB_STATE/status"
return 1
fi
local other
printf '\033[2J\033[HRetroBIOS\n\nChecking the connection...\n'
if ! rb_online; then
printf 'No connection. Looking for the BIOS folder and a pack on the card...\n'
# Detection also names the BIOS folder the other card must not be.
rb_detect
other=$(rb_other_card)
@@ -480,6 +757,10 @@ rb_here() {
args=(--from "$other")
fi
pack=$(rb_find_pack)
if rb_stop_requested; then
printf '143\n' > "$RB_STATE/status"
return 143
fi
if [ -z "$pack" ]; then
if [ "${#args[@]}" -gt 0 ]; then
# --from needs the list of files, which comes from the network or a pack.
@@ -495,7 +776,7 @@ rb_here() {
args+=(--platform "$RB_PLATFORM")
fi
fi
rb_py "$RB_INSTALL" --tui "${args[@]}" "$@" 2> "$RB_STATE/tui.err"
rb_interface "${args[@]}" "$@"
status=$?
cat "$RB_STATE/tui.err" >> "$RB_LOG" 2>/dev/null
if [ "$status" = "2" ] && grep -q -- "--tui cannot open" "$RB_STATE/tui.err"; then
@@ -503,59 +784,117 @@ rb_here() {
if [ "${RB_FALLBACK:-0}" != "1" ]; then
rb_message "$(tail -n 1 "$RB_STATE/tui.err") The messages are in $RB_LOG."
fi
elif [ -n "$RB_STOPPED" ] || [ "$status" -ge 128 ]; then
# Ended by Start + Select or a signal: the player asked to leave, and no
# button may be left to close a message.
:
elif [ "$status" != "0" ] && [ -s "$RB_STATE/tui.err" ]; then
rb_message "$(tail -n 1 "$RB_STATE/tui.err") The messages are in $RB_LOG."
# The first line of the farewell: the error screen it closes was read.
rb_message "$(head -n 1 "$RB_STATE/tui.err") The messages are in $RB_LOG."
fi
printf '%s\n' "$status" > "$RB_STATE/status"
return "$status"
}
rb_cleanup() {
rb_ui_stop
if rb_alive "$RB_APP_PID"; then
kill -TERM "$RB_APP_PID" 2>/dev/null
wait "$RB_APP_PID" 2>/dev/null
fi
RB_APP_PID=""
rb_vt_leave
rb_mapper_stop
}
# The whole session: the terminal, the mapper, the interface, then the
# console, its font and the mapper put back. Returns the interface's status,
# 125 when no terminal could show it.
rb_launch() {
local status
mkdir -p "$RB_STATE"
if ! rb_route; then
printf '%s\n' "RetroBIOS: no terminal to show the interface on (routes: $RB_ROUTES)" >> "$RB_LOG"
return 125
fi
export RB_DIR RB_INSTALL RB_EVENTS RB_STATE RB_LOG RB_PACK_PATH RB_FALLBACK
export RB_OTHER_CARDS RB_BIOS_DEST
export RB_PYTHON RB_PYTHONHOME RB_PYTHON_PROBLEM RB_MESSAGE_WAIT RB_ROUTE_TABLE
trap 'rb_cleanup; exit 143' TERM HUP
rb_mapper_start
# One route: sets RB_ROUTE_FAILED when it could not show the interface, so
# the next route is tried.
rb_route_run() {
local status code
RB_ROUTE_FAILED=""
case "$RB_ROUTE" in
here)
rb_here "$@"
status=$?
;;
app)
"${RB_TERMAPP[@]}" bash "$RB_LIB" here "$@" 2>> "$RB_LOG"
status=$(cat "$RB_STATE/status" 2>/dev/null)
rm -f "$RB_STATE/status"
"${RB_TERMAPP[@]}" bash "$RB_LIB" here "$@" 2>> "$RB_LOG" &
RB_APP_PID=$!
wait "$RB_APP_PID"
code=$?
RB_APP_PID=""
if [ -f "$RB_STATE/status" ]; then
status=$(cat "$RB_STATE/status")
else
# foot or xterm refused to open (no display, no XAUTHORITY): the
# interface never ran.
rb_say "${RB_TERMAPP[0]} ended with status $code before the interface ran"
RB_ROUTE_FAILED=1
return 1
fi
;;
vt)
rb_vt_enter
if ! rb_vt_enter; then
RB_ROUTE_FAILED=1
return 1
fi
export TERM=linux
# shellcheck disable=SC2094 # a console is read and written at once by design
rb_here "$@" < "$RB_TTY" > "$RB_TTY" 2>> "$RB_LOG"
status=$?
rb_vt_leave
;;
esac
return "${status:-1}"
}
# The whole session: the terminal, the mapper, the interface, then the
# console, its font and the mapper put back. Returns the interface's status,
# 125 when no terminal could show it.
rb_launch() {
local status="" route
RB_STOPPED=""
if [ -n "$RB_STATE_PROBLEM" ]; then
printf '%s\n' "$RB_STATE_PROBLEM" >&2
if rb_is_console "$RB_CONSOLE"; then
printf '\r\n%s\r\n' "$RB_STATE_PROBLEM" > "$RB_CONSOLE" 2>/dev/null
fi
return 1
fi
mkdir -p "$RB_STATE"
export RB_DIR RB_INSTALL RB_EVENTS RB_STATE RB_LOG RB_PACK_PATH RB_FALLBACK
export RB_OTHER_CARDS RB_BIOS_DEST RB_STOP_GRACE RB_DEAD_WAIT RB_DETECT_LIMIT
export RB_PYTHON RB_PYTHONHOME RB_PYTHON_PROBLEM RB_MESSAGE_WAIT RB_ROUTE_TABLE
trap 'rb_cleanup; exit 143' TERM HUP
for route in $RB_ROUTES; do
rb_route_ready "$route" || continue
rb_mapper_start
export RB_MAPPER RB_MAPPER_PID RB_MAPPER_NEEDED RB_STOP_PID
RB_ROUTE="$route"
rb_say "showing the interface by the $route route"
rb_route_run "$@"
status=$?
[ -z "$RB_ROUTE_FAILED" ] && break
status=""
done
rb_cleanup
trap - TERM HUP
return "${status:-1}"
if [ -z "$status" ]; then
RB_ROUTE="none"
rb_say "no terminal to show the interface on (routes: $RB_ROUTES)"
return 125
fi
return "$status"
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
case "${1:-}" in
here)
shift
# The terminal app closed or the launcher signalled: the interface
# gets its time to stop.
trap 'rb_ui_stop; exit 143' TERM HUP
rb_here "$@"
exit $?
;;
+2 -1
View File
@@ -13,7 +13,8 @@ the missing ones: downloaded with a network connection, or from a pack copied
into the `retrobios` folder without one.
Controls: D-pad to move, A or Start to choose, B or Select to go back, X to
tick a system, Y for the options, L1 and R1 to page, Start + Select to quit.
tick a system, Y for the options, L1 and R1 to page. To quit, hold Select, then press Start: Start pressed
first would also choose the line in focus.
ArkOS has Python 3.7, older than the installer needs: RetroBIOS uses the
Python runtime the RetroBIOS PortMaster port downloads, so start that port
+24 -4
View File
@@ -15,6 +15,8 @@ ROOT="${RB_ROOT:-}"
RB_DIR="$HERE/retrobios"
RB_LOG="$RB_DIR/log.txt"
ESUDO=""
MAPPER_SUDO=""
kill_flag="-1"
OS_NAME=""
if [ -f "$ROOT/etc/os-release" ]; then
OS_NAME=$(awk -F= '$1 == "NAME" { gsub(/"/, "", $2); print $2 }' "$ROOT/etc/os-release")
@@ -49,6 +51,10 @@ if [ -d "$ROOT/opt/system" ] && [ -d "$ROOT/opt/inttools" ]; then
mapper=$(rb_gptokeyb "$ROOT/opt/inttools/gptokeyb" "$controlfolder/gptokeyb")
[ -f "$ROOT/opt/inttools/gamecontrollerdb.txt" ] \
&& export SDL_GAMECONTROLLERCONFIG_FILE="$ROOT/opt/inttools/gamecontrollerdb.txt"
# sudo's env_reset drops the controller list exported above, and a pad
# SDL does not know then sends nothing: PortMaster keeps it the same way
# on ArkOS (control.txt, ESUDO).
[ -n "$ESUDO" ] && MAPPER_SUDO="sudo --preserve-env=SDL_GAMECONTROLLERCONFIG_FILE"
elif [ "$OS_NAME" = "AmberELEC" ]; then
RB_ROUTES="vt"
RB_CONSOLE="${RB_CONSOLE:-/dev/console}"
@@ -65,15 +71,29 @@ else
RB_PACK_PATH="$RB_DIR:$HERE:/storage/roms"
controlfolder="/storage/roms/ports/PortMaster"
mapper=$(rb_gptokeyb "$ROOT/usr/bin/gptokeyb" "$controlfolder/gptokeyb")
# ROCKNIX's own tools load the pad's controller list first (fex-emu's
# Start FEXConfig.sh: control-gen_init.sh, control.ini, get_controls), and
# start the GPTOKEYB it names. control.ini also sets ESUDO and
# controlfolder, which this launcher keeps as they were.
control="$ROOT/storage/.config/gptokeyb/control.ini"
command -v control-gen_init.sh >/dev/null 2>&1 && control-gen_init.sh >/dev/null 2>&1
if [ -f "$control" ]; then
saved_esudo="$ESUDO" saved_folder="$controlfolder"
# shellcheck source=/dev/null
source "$control"
declare -F get_controls >/dev/null && get_controls
# It carries its own kill flag (-sudokill).
[ -n "${GPTOKEYB:-}" ] && mapper="$GPTOKEYB" kill_flag=""
ESUDO="$saved_esudo" controlfolder="$saved_folder"
fi
fi
# Kill mode on the installer's own path: Start + Select closes the
# interface, never another program.
# Kill mode: Start + Select closes the interface, never another program.
RB_MAPPER=""
[ -n "$mapper" ] && RB_MAPPER="$ESUDO $mapper -1"
[ -n "$mapper" ] && RB_MAPPER="${MAPPER_SUDO:-$ESUDO} $mapper $kill_flag"
# shellcheck source=../shared/tui.txt
source "$RB_DIR/tui.txt"
: > "$RB_LOG"
rb_open_log fresh
rb_select_python
# A launcher's own arguments are not install.py's (muOS passes the app folder).
# shellcheck disable=SC2119
-1
View File
@@ -103,7 +103,6 @@ def archive_members(name: str, root: Path = REPO_ROOT) -> list[tuple[str, Path]]
source = ports / "muos"
members = [
("RetroBIOS/mux_launch.sh", source / "mux_launch.sh"),
("RetroBIOS/muxterm.gptk", source / "muxterm.gptk"),
("RetroBIOS/README.md", source / "README.md"),
*_engine("RetroBIOS", root),
]
File diff suppressed because it is too large. Load diff