Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,78 @@ echo "$(date): launched $HOOK_ROM_PATH" >> "$LOGS_PATH/launches.log"

## Rules

- Each script runs in a subshell. A crash or non-zero exit will not affect the launcher or other hooks.
- Each script runs in a subshell. A crash will not affect the launcher or other hooks.
- Script output (stdout/stderr) is suppressed. If you need logging, write to your own log file.
- Pre-launch hooks cannot cancel the launch. They are for observation and setup only.
- A **synchronous** pre-launch hook (`*.sync.sh`) that exits non-zero **cancels the launch**: the rom or pak is never started, and `post-launch.d` is skipped. See "Vetoing a launch" below.
- Background hooks cannot cancel anything — their exit status is not recoverable from `wait` in POSIX sh, so only `.sync.sh` hooks get a vote. A non-zero exit from a background hook is ignored, as is any exit status outside `pre-launch.d`.
- Keep hooks fast. A slow hook delays the launch or the return to the menu.
- Unlike auto.sh, each pak should manage their own hook and use a descriptive filename to avoid collisions.


## Vetoing a launch

Name the hook `*.sync.sh` and exit non-zero:

```sh
#!/bin/sh
# no-roms-before-noon.sync.sh

[ "$HOOK_TYPE" = "rom" ] || exit 0
[ "$(date +%H)" -ge 12 ] && exit 0

show2.elf --mode=simple --text="Not before noon" --timeout=3
exit 1
```

Two things to know when you cancel a rom launch:

- The launcher has already run `gametimectl.elf start` for that rom by the time the
hook runs, so a vetoing hook should call `gametimectl.elf stop_all` to avoid leaving
an open play session behind.
- Nothing is displayed for you. If the user should know why nothing happened, say so —
`show2.elf` is the usual way.


## Pak-scoped hooks (self-registering, no arming step)

The `.hooks/` directories above require a pak to actively install its own
script there (usually the first time it's opened), and nothing removes that
script when the pak is later deleted -- it's an orphan until someone opens
the pak again to tidy up after itself.

For a Tools pak that wants a **launch gate** and/or a **teardown step** every
single time, there's a second, self-registering mechanism: drop a
`pre-launch.sh` and/or `post-launch.sh` file right next to the pak's own
`launch.sh`:

```
Tools/tg5040/SomePak.pak/
launch.sh
pre-launch.sh # optional
post-launch.sh # optional
```

`pak-hooks.sh` scans every installed Tools pak for these two filenames and
runs whichever exist, in alphabetical order by pak folder name, on every ROM
or pak launch -- same env vars as above (`HOOK_TYPE`, `HOOK_ROM_PATH`, etc.).
No copy step, no install step: the file's presence in the pak's own folder
*is* the registration, and removing the pak removes its hook with it.

Differences from `.hooks/*.d/`:

- No `*.sync.sh` naming trick: `pre-launch.sh` is always synchronous and
always gets a vote. A non-zero exit from **any** installed pak's
`pre-launch.sh` cancels the launch -- every pak that registers one is
assumed to need to agree before a game runs.
- The list of paks with a registered hook is cached (`/tmp/pak_hooks_cache.txt`)
and rebuilt when returning to the main menu, not on every single launch --
see `nextui.c`. A pak installed or removed while sitting at the menu is
picked up on the very next visit, not stuck until reboot.
- Because presence alone activates it, installing a pak with a
`pre-launch.sh` is enough to give it veto power over every launch -- there
is no separate "open once to arm" consent step like some paks used to
implement by hand via `.hooks/`.

## Example: sync after ROM exit

```sh
Expand Down
3 changes: 3 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ ifneq ($(PLATFORM), desktop)
cp ./workspace/all/libgametimedb/build/$(PLATFORM)/libgametimedb.so ./build/SYSTEM/$(PLATFORM)/lib
cp ./workspace/all/gametimectl/build/$(PLATFORM)/gametimectl.elf ./build/SYSTEM/$(PLATFORM)/bin/
cp ./workspace/all/gametime/build/$(PLATFORM)/gametime.elf ./build/EXTRAS/Tools/$(PLATFORM)/Game\ Tracker.pak/

# daily play time budget (reads the same db as game time tracking)
cp ./workspace/all/parental/build/$(PLATFORM)/parental.elf ./build/EXTRAS/Tools/$(PLATFORM)/Parental.pak/
endif
cp ./workspace/$(PLATFORM)/libmsettings/libmsettings.so ./build/SYSTEM/$(PLATFORM)/lib
cp ./workspace/all/nextui/build/$(PLATFORM)/nextui.elf ./build/SYSTEM/$(PLATFORM)/bin/
Expand Down
5 changes: 5 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Parental.pak/launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh

cd "$(dirname "$0")"

./parental.elf # &> ./log.txt
9 changes: 9 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Parental.pak/post-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
# post-launch.sh -- tear down the watcher started by pre-launch.sh

[ "$HOOK_TYPE" = "rom" ] || exit 0

PAK="$SDCARD_PATH/Tools/$PLATFORM/Parental.pak"
[ -x "$PAK/parental.elf" ] || exit 0

"$PAK/parental.elf" --stop
37 changes: 37 additions & 0 deletions skeleton/EXTRAS/Tools/tg5040/Parental.pak/pre-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh
# pre-launch.sh -- refuse a rom launch once the daily budget is spent.
#
# Scanned and run by pak-hooks.sh: a non-zero exit here cancels the launch.

[ "$HOOK_TYPE" = "rom" ] || exit 0

# Emulators the owner exempted in the settings screen. HOOK_EMU_PATH points at
# the emulator's launch.sh, so the pak name is its parent directory -- basename
# alone would be "launch.sh" for every emulator alike.
#
# This is what lets tool shortcuts through: they are launched by the bridge
# emulator of the Shortcuts pak, which makes them look like roms to the launcher.
# Deny by default, so a pak that is not named here stays blocked.
EMU_PAK=$(basename "$(dirname "$HOOK_EMU_PATH")")
EXEMPT=$(sed -n 's/^exempt=//p' "$SHARED_USERDATA_PATH/parental.txt" 2>/dev/null)
case ",$EXEMPT," in
*",$EMU_PAK,"*) exit 0 ;;
esac

PAK="$SDCARD_PATH/Tools/$PLATFORM/Parental.pak"
[ -x "$PAK/parental.elf" ] || exit 0

if "$PAK/parental.elf" --gate; then
# budget left: watch the session so it can be stopped on time
"$PAK/parental.elf" --watch &
exit 0
fi

# gametimectl.elf start only runs after every pre-launch hook (including this
# one) succeeds, so a refused launch never opens a play_activity row -- no
# compensating stop_all needed here anymore.

# --image is mandatory, show2.elf prints its usage and draws nothing without it
show2.elf --mode=simple --image="$SDCARD_PATH/.system/res/logo.png" --text="No play time left today" --timeout=3

exit 1
5 changes: 5 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Parental.pak/launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh

cd "$(dirname "$0")"

./parental.elf # &> ./log.txt
9 changes: 9 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Parental.pak/post-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
# post-launch.sh -- tear down the watcher started by pre-launch.sh

[ "$HOOK_TYPE" = "rom" ] || exit 0

PAK="$SDCARD_PATH/Tools/$PLATFORM/Parental.pak"
[ -x "$PAK/parental.elf" ] || exit 0

"$PAK/parental.elf" --stop
37 changes: 37 additions & 0 deletions skeleton/EXTRAS/Tools/tg5050/Parental.pak/pre-launch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh
# pre-launch.sh -- refuse a rom launch once the daily budget is spent.
#
# Scanned and run by pak-hooks.sh: a non-zero exit here cancels the launch.

[ "$HOOK_TYPE" = "rom" ] || exit 0

# Emulators the owner exempted in the settings screen. HOOK_EMU_PATH points at
# the emulator's launch.sh, so the pak name is its parent directory -- basename
# alone would be "launch.sh" for every emulator alike.
#
# This is what lets tool shortcuts through: they are launched by the bridge
# emulator of the Shortcuts pak, which makes them look like roms to the launcher.
# Deny by default, so a pak that is not named here stays blocked.
EMU_PAK=$(basename "$(dirname "$HOOK_EMU_PATH")")
EXEMPT=$(sed -n 's/^exempt=//p' "$SHARED_USERDATA_PATH/parental.txt" 2>/dev/null)
case ",$EXEMPT," in
*",$EMU_PAK,"*) exit 0 ;;
esac

PAK="$SDCARD_PATH/Tools/$PLATFORM/Parental.pak"
[ -x "$PAK/parental.elf" ] || exit 0

if "$PAK/parental.elf" --gate; then
# budget left: watch the session so it can be stopped on time
"$PAK/parental.elf" --watch &
exit 0
fi

# gametimectl.elf start only runs after every pre-launch hook (including this
# one) succeeds, so a refused launch never opens a play_activity row -- no
# compensating stop_all needed here anymore.

# --image is mandatory, show2.elf prints its usage and draws nothing without it
show2.elf --mode=simple --image="$SDCARD_PATH/.system/res/logo.png" --text="No play time left today" --timeout=3

exit 1
56 changes: 56 additions & 0 deletions skeleton/SYSTEM/desktop/bin/pak-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/sh
# pak-hooks.sh - self-registering, pak-scoped launch hooks
#
# Unlike run_hooks.sh (scripts dropped into $USERDATA_PATH/.hooks/, which any
# pak has to arm/disarm itself), this scans installed Tools paks for a
# pre-launch.sh and/or post-launch.sh file sitting right next to their
# launch.sh. Presence of the file *is* the registration: no install step, and
# removing the pak removes its hook with it.
#
# Usage: pak-hooks.sh rebuild-cache
# pak-hooks.sh pre-launch (exit != 0 vetoes the launch)
# pak-hooks.sh post-launch
#
# The list of qualifying paks is cached to avoid re-scanning every Tools pak
# on every single launch. The cache is rebuilt when returning to the main
# menu (see nextui.c), not at launch time, so a freshly installed/removed pak
# is picked up on the very next visit to the menu instead of going stale
# until the next reboot.

: "${SDCARD_PATH:=/var/tmp/nextui/sdcard}"
: "${PLATFORM:=desktop}"
CACHE="/tmp/pak_hooks_cache.txt"

rebuild_cache() {
: > "$CACHE"
for pak in "$SDCARD_PATH"/Tools/"$PLATFORM"/*.pak; do
[ -d "$pak" ] || continue
if [ -x "$pak/pre-launch.sh" ] || [ -x "$pak/post-launch.sh" ]; then
echo "$pak" >> "$CACHE"
fi
done
}

run_phase() {
PHASE="$1" # pre-launch | post-launch
SCRIPT_NAME="$PHASE.sh"
export HOOK_PHASE="${PHASE%-launch}"

[ -f "$CACHE" ] || rebuild_cache

VETOED=0
while IFS= read -r pak; do
[ -n "$pak" ] || continue
[ -x "$pak/$SCRIPT_NAME" ] || continue
if ! "$pak/$SCRIPT_NAME"; then
VETOED=1
fi
done < "$CACHE"

exit $VETOED
}

case "$1" in
rebuild-cache) rebuild_cache ;;
pre-launch|post-launch) run_phase "$1" ;;
esac
14 changes: 13 additions & 1 deletion skeleton/SYSTEM/desktop/bin/run_hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#
# By default, scripts run in the background. Scripts ending in .sync.sh
# always run synchronously. All background scripts are waited on before exit.
#
# Veto: a synchronous hook that exits non-zero makes run_hooks.sh itself exit
# non-zero, letting the caller cancel whatever the hooks were run ahead of
# (see pre-launch.d in MinUI.pak/launch.sh). Background hooks cannot veto --
# their exit status is not recoverable from `wait` in POSIX sh -- so only
# .sync.sh hooks (or any hook under --sync-only) get a vote.

DIR_NAME="$1"
SYNC_ONLY="${2:-}"
Expand All @@ -25,12 +31,18 @@ case "$DIR_NAME" in
esac
export HOOK_CATEGORY="$DIR_NAME"

VETOED=0

for script in "$HOOK_DIR"/*.sh; do
[ -f "$script" ] || continue
if [ "$SYNC_ONLY" = "--sync-only" ] || echo "$script" | grep -q '\.sync\.sh$'; then
( "$script" ) > /dev/null 2>&1 || true
if ! ( "$script" ) > /dev/null 2>&1; then
VETOED=1
fi
else
( "$script" ) > /dev/null 2>&1 &
fi
done
wait

exit $VETOED
13 changes: 10 additions & 3 deletions skeleton/SYSTEM/desktop/paks/MinUI.pak/launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,16 @@ touch "$EXEC_PATH" && sync
if [ -f $NEXT_PATH ]; then
CMD=`cat $NEXT_PATH`
parse_hook_cmd "$CMD"
"$SYSTEM_PATH/bin/run_hooks.sh" pre-launch.d
eval $CMD
"$SYSTEM_PATH/bin/run_hooks.sh" post-launch.d
# a synchronous pre-launch hook that exits non-zero cancels the launch
if "$SYSTEM_PATH/bin/run_hooks.sh" pre-launch.d && "$SYSTEM_PATH/bin/pak-hooks.sh" pre-launch; then
# only start tracking once nothing vetoed the launch -- a refused
# rom no longer needs a compensating gametimectl stop_all
[ "$HOOK_TYPE" = "rom" ] && gametimectl.elf start "$HOOK_ROM_PATH"
eval $CMD
"$SYSTEM_PATH/bin/run_hooks.sh" post-launch.d
"$SYSTEM_PATH/bin/pak-hooks.sh" post-launch
[ "$HOOK_TYPE" = "rom" ] && gametimectl.elf stop "$HOOK_ROM_PATH"
fi
rm -f $NEXT_PATH
fi
#done
56 changes: 56 additions & 0 deletions skeleton/SYSTEM/tg5040/bin/pak-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/sh
# pak-hooks.sh - self-registering, pak-scoped launch hooks
#
# Unlike run_hooks.sh (scripts dropped into $USERDATA_PATH/.hooks/, which any
# pak has to arm/disarm itself), this scans installed Tools paks for a
# pre-launch.sh and/or post-launch.sh file sitting right next to their
# launch.sh. Presence of the file *is* the registration: no install step, and
# removing the pak removes its hook with it.
#
# Usage: pak-hooks.sh rebuild-cache
# pak-hooks.sh pre-launch (exit != 0 vetoes the launch)
# pak-hooks.sh post-launch
#
# The list of qualifying paks is cached to avoid re-scanning every Tools pak
# on every single launch. The cache is rebuilt when returning to the main
# menu (see nextui.c), not at launch time, so a freshly installed/removed pak
# is picked up on the very next visit to the menu instead of going stale
# until the next reboot.

: "${SDCARD_PATH:=/mnt/SDCARD}"
: "${PLATFORM:=tg5040}"
CACHE="/tmp/pak_hooks_cache.txt"

rebuild_cache() {
: > "$CACHE"
for pak in "$SDCARD_PATH"/Tools/"$PLATFORM"/*.pak; do
[ -d "$pak" ] || continue
if [ -x "$pak/pre-launch.sh" ] || [ -x "$pak/post-launch.sh" ]; then
echo "$pak" >> "$CACHE"
fi
done
}

run_phase() {
PHASE="$1" # pre-launch | post-launch
SCRIPT_NAME="$PHASE.sh"
export HOOK_PHASE="${PHASE%-launch}"

[ -f "$CACHE" ] || rebuild_cache

VETOED=0
while IFS= read -r pak; do
[ -n "$pak" ] || continue
[ -x "$pak/$SCRIPT_NAME" ] || continue
if ! "$pak/$SCRIPT_NAME"; then
VETOED=1
fi
done < "$CACHE"

exit $VETOED
}

case "$1" in
rebuild-cache) rebuild_cache ;;
pre-launch|post-launch) run_phase "$1" ;;
esac
14 changes: 13 additions & 1 deletion skeleton/SYSTEM/tg5040/bin/run_hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
#
# By default, scripts run in the background. Scripts ending in .sync.sh
# always run synchronously. All background scripts are waited on before exit.
#
# Veto: a synchronous hook that exits non-zero makes run_hooks.sh itself exit
# non-zero, letting the caller cancel whatever the hooks were run ahead of
# (see pre-launch.d in MinUI.pak/launch.sh). Background hooks cannot veto --
# their exit status is not recoverable from `wait` in POSIX sh -- so only
# .sync.sh hooks (or any hook under --sync-only) get a vote.

DIR_NAME="$1"
SYNC_ONLY="${2:-}"
Expand All @@ -25,12 +31,18 @@ case "$DIR_NAME" in
esac
export HOOK_CATEGORY="$DIR_NAME"

VETOED=0

for script in "$HOOK_DIR"/*.sh; do
[ -f "$script" ] || continue
if [ "$SYNC_ONLY" = "--sync-only" ] || echo "$script" | grep -q '\.sync\.sh$'; then
( "$script" ) > /dev/null 2>&1 || true
if ! ( "$script" ) > /dev/null 2>&1; then
VETOED=1
fi
else
( "$script" ) > /dev/null 2>&1 &
fi
done
wait

exit $VETOED
Loading