#!/bin/sh
# ps3-game-guard run|leave|join|add|quit (ps3-desktop): game mode's user side. Games come to game
# mode two ways: started through ps3-game (the menu's launchers), or noticed by ps3-game-detect
# (any fullscreen 3D program). Both register the game here with `join`; the first one starts
# `run` as the transient user service ps3-game-mode.service (in ps3-game.slice, so never frozen
# itself), with `leave` as its ExecStopPost.
#   join ID MODE [PID[:START]]...  registers game ID ($S/games/ID: one pid per line, with its
#          start time for a detected game) and returns once game mode is on (MODE: on or pads,
#          used when this join starts it), at most 30 s later. Exit status 1: game mode didn't come.
#   add ID PID   one more pid for game ID (ps3-game: the game's own, once it runs)
#   quit ID      unregisters game ID; undoes game mode left behind by a guard that died
#   away | back  ps3-game-detect's, for games started through ps3-game: no game window in front
#          for 3 s (Alt+Tab to another window, Win+D: show the desktop) thaws the desktop while
#          the game keeps game mode; a game window in front again freezes it again (as entering
#          does: X checked first). A game the detector found leaves game mode instead.
#   run    enters game mode, then waits until no registered game is alive. A pid line counts
#          while that process runs in ps3-game.slice (ps3-game's launcher and game), or, with a
#          start time, while that very process runs anywhere and isn't a zombie (a detected game:
#          one started from outside the user manager can't be moved into ps3-game.slice). Two
#          games: game mode lasts until the last one is gone.
#   leave  brings everything back. systemd runs it however the service ends: the last game gone,
#          the guard killed, the user manager stopping. ps3-game-mode (root) has a watcher of
#          its own that thaws the system services even if this never runs.
# Entering, in order (a game in ps3-desktop.slice has been moved out of it before its join):
#   1. the gamepads off the X pointer (xinput), so the sticks don't move the cursor in the game
#   2. the PCManFM desktop off (pcmanfm --desktop-off): its window and wallpaper go, and with no
#      file manager window open the whole process exits. The root window gets a plain colour,
#      else X keeps the wallpaper (pcmanfm sets it as the root background too): 8 MB of picture
#      in VRAM or GART. leave starts pcmanfm --desktop again (icons and wallpaper come from its
#      configuration; nothing is saved or changed)
#   3. ps3-desktop.slice frozen (cgroup.freeze, like SIGSTOP: the panel, tray applets,
#      notifications, polkit agent, gvfs, file manager windows and terminals keep their state)
#      and its memory pushed out: anonymous pages to swap first, then its file pages.
#      Never with X held by one of its programs, which would keep it frozen: X without input, or
#      without any answer (2026-10-18: Luanti from the Games menu found no X; jgmenu frozen).
#      jgmenu (the menu, which stays running hidden between uses) is closed first (SIGTERM) and
#      started again hidden on leave, as it was. Then ps3-x-ready looks at X: a grab held by an
#      open popup of the desktop (a tray applet's menu), or X not answering at all (a server
#      grab), defers the freeze, at most 5 s here, then retried every 3 s for a minute while the
#      game runs (the rest of game mode goes on meanwhile). Right after freezing it looks again:
#      X held then (a program frozen in the middle of a grab) thaws the desktop, retried the same.
#   4. the user manager's own services (the user bus; app.slice, session.slice) keep running,
#      their anonymous memory pushed to swap; they fault back in what they use
#   5. root (sudo ps3-game-mode on): system services' share (see ps3-game-mode)
#   Before step 2, the desktop's files and which of their pages are in memory are recorded
#   (ps3-game-prefetch), and leave reads those back in one pass in disk order while the desktop
#   thaws: otherwise the thawed programs fault them in one by one from the hard disk.
# From the join that starts the guard (the game's environment):
#   PS3_GAME_MODE=pads: step 1 only, a baseline to measure against.
#   PS3_GAME_KICK=on|off|force: the 10 ms kicker for the lost-decrementer stalls (ps3-game-mode);
#   default: /etc/default/ps3-game-mode, else on (not while the kernel has the idle fix).
#   PS3_GAME_PREFETCH=on (default): record, reclaim all, fetch back on leave; off: no record, no
#   fetch (game mode as it was before the prefetch); keep: the desktop keeps its file pages (only its anonymous memory
#   goes to swap; the recorded size says what that costs), fetch what got evicted anyway.
# The games' own files (docs/STALLS.md "Game prefetch"; not with PS3_GAME_MODE=pads): once entered,
# `ps3-game-prefetch game-watch` claims each registered game ($S/gp/<id>.seen), has the files it
# used last time read in one pass in disk order at low I/O priority while it loads, and looks at
# it for its first minutes; it exits when no game is left to look at, and run starts another for a
# game registered later that none has claimed. leave, once the desktop is back, learns every
# record into its game's list (game-learn). Per game, from its environment:
# PS3_GAME_PREFETCH_GAME=on|fetch|learn|off (default off: no gain measured), PS3_GAME_PREFETCH_MB, _RESERVE_MB, _IO, _DIR
# (ps3_game_prefetch.py's header).
# Real-time audio (docs/SPU-AUDIO.md "Real-time mixer"): while game mode is on (on or pads), the
# games' audio threads get SCHED_RR 1 (ps3-rt-audio scan: every 2 s for the first minute, then
# every 10 s), under the user's RLIMIT_RTPRIO (limits.d/ps3-rt-audio.conf). PS3_GAME_RT_AUDIO=off in
# a game's environment: not its threads (in the environment of the game that starts game mode: no
# game's).
# Escape (docs/DESKTOP-PLAN.md): while game mode is on, ps3-pad-escape runs beside the guard
# (PS3_GAME_PAD_ESCAPE=off: not): holding a gamepad's PS button for 3 s (PS3_GAME_PAD_HOLD_S) runs
# ps3-game-escape, as Ctrl+Alt+Esc does (Openbox): the game closed, game mode left.
# State: $S (in the runtime directory: gone at logout); log: the user journal.
M=/usr/lib/ps3/sbin/ps3-game-mode
P=/usr/lib/ps3/desktop/ps3-game-prefetch
S=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/ps3-game
log() { echo "ps3-game-guard: $*" >&2; }

slice_cg() {	# the cgroup directory of a user unit, if it has one
	p=$(systemctl --user show -P ControlGroup "$1" 2>/dev/null)
	[ -n "$p" ] && [ -d "/sys/fs/cgroup$p" ] && echo "/sys/fs/cgroup$p"
}

# reclaim CG: as much as it gives up. Anonymous pages first (swappiness=200: swap only): left to
# itself the kernel would rather drop file pages, the game's own code among them, and keep the
# frozen helpers' heaps. Then its file pages; pages others use stay (referenced pages survive).
reclaim() {
	cur=$(cat "$1/memory.current" 2>/dev/null) || return 0
	timeout 30 sh -c 'echo "$1 swappiness=200" > "$2/memory.reclaim"' - "$cur" "$1" 2>/dev/null
	cur=$(cat "$1/memory.current" 2>/dev/null) || return 0
	timeout 30 sh -c 'echo "$1" > "$2/memory.reclaim"' - "$cur" "$1" 2>/dev/null
	cur=$(cat "$1/memory.current" 2>/dev/null) && log "reclaimed ${1##*/}: $((cur / 1024)) kB left"
}

reclaim_anon() {	# anonymous memory only: running services, and the desktop with PS3_GAME_PREFETCH=keep
	anon=$(awk '$1 == "anon" { print $2 }' "$1/memory.stat" 2>/dev/null)
	[ -n "$anon" ] && timeout 30 sh -c 'echo "$1 swappiness=200" > "$2/memory.reclaim"' - "$anon" "$1" 2>/dev/null
	return 0
}

# x_ready CG: nothing of CG holds X (ps3-x-ready, see the top); else 1, and XWHY says what does.
# A probe that fails by itself (no Python, no display, slow to start) doesn't hold the freeze up.
XR=/usr/lib/ps3/desktop/ps3-x-ready
x_ready() {
	[ -n "${DISPLAY:-}" ] && [ -x "$XR" ] || return 0
	out=$(timeout 6 "$XR" "${1#/sys/fs/cgroup}" 2>&1); rc=$?
	case $rc/$out in
	124/start*) XWHY="X doesn't answer (a server grab?)"; return 1 ;;
	1/start*held*) XWHY=$(echo "$out" | sed 1d); return 1 ;;
	esac
	return 0
}

# menus_off CG: jgmenu in CG closed (it runs hidden between uses and holds the keyboard and
# pointer while open: frozen open, it would keep them). Its environment is kept for menus_on.
menus_off() {
	rel=${1#/sys/fs/cgroup} ps=
	for p in $(pgrep -u "$(id -u)" -x jgmenu); do
		read -r c 2>/dev/null < "/proc/$p/cgroup" || continue
		case ${c#0::} in "$rel"/*) ;; *) continue ;; esac
		[ -s "$S/jgmenu.env" ] || cat "/proc/$p/environ" > "$S/jgmenu.env" 2>/dev/null
		kill -TERM "$p" && ps="$ps $p"
	done
	[ -n "$ps" ] || return 0
	i=0
	while [ $i -lt 30 ]; do
		n=0
		for p in $ps; do case $(proc "$p") in Z\ *|X\ *|'') ;; *) n=1 ;; esac; done
		[ $n = 0 ] && break
		sleep 0.1; i=$((i + 1))
	done
	if [ $n = 1 ]; then	# SIGKILL: its lockfile stays (jgmenu_run copes, menus_on too)
		# shellcheck disable=SC2086 # pids
		kill -KILL $ps 2>/dev/null
		log "jgmenu killed (pid$ps): it didn't exit"
	else
		log "jgmenu closed (pid$ps)"
	fi
}
# menus_on: jgmenu started again hidden (as it ran before; the panel's button shows it), in a scope
# of ps3-desktop.slice with the environment it had, if none runs by now
menus_on() {
	[ -e "$S/jgmenu.env" ] || return 0
	if ! pgrep -u "$(id -u)" -x jgmenu > /dev/null; then
		rm -f "$HOME/.jgmenu-lockfile"
		set --
		while IFS= read -r kv; do [ -n "$kv" ] && set -- "$@" "$kv"; done <<END_ENV
$(tr '\0' '\n' < "$S/jgmenu.env")
END_ENV
		if in_desktop env -i "$@" nice -n 10 jgmenu --hide-on-startup; then
			log "leave: jgmenu started again (hidden)"
		else
			log "jgmenu didn't start again (the panel's button starts it)"
		fi
	fi
	rm -f "$S/jgmenu.env"
}

freeze() {	# freeze CG and wait (up to 5 s) until it is
	echo 1 > "$1/cgroup.freeze" 2>/dev/null || return 1
	i=0
	while [ $i -lt 50 ] && ! grep -q '^frozen 1' "$1/cgroup.events"; do sleep 0.1; i=$((i + 1)); done
}

# the desktop's PCManFM: the instance ps3-session started (exactly this command line; one
# started otherwise is left alone, both ways)
desktop() { pgrep -u "$(id -u)" -x -f '(/[^ ]*/)?pcmanfm --desktop' >/dev/null; }
desktop_off() {
	[ -n "${DISPLAY:-}" ] && desktop || return 0
	: > "$S/desktop"
	timeout 20 pcmanfm --desktop-off >/dev/null 2>&1
	i=0	# it exits within moments unless a file manager window keeps it
	while [ $i -lt 20 ] && desktop; do sleep 0.1; i=$((i + 1)); done
	xsetroot -solid '#14304d'
	xprop -root -remove _XROOTPMAP_ID 2>/dev/null; xprop -root -remove _XROOTMAP_ID 2>/dev/null
}
# in_desktop COMMAND...: started in a scope of ps3-desktop.slice, as ps3-session starts the
# desktop's programs. Waited for until it has left this cgroup: systemd kills what an
# ExecStopPost leaves behind.
in_desktop() {
	read -r me 2>/dev/null < /proc/self/cgroup
	setsid systemd-run --user --scope --slice=ps3-desktop.slice -q "$@" < /dev/null > /dev/null 2>&1 &
	p=$! i=0
	while [ $i -lt 100 ] && read -r c 2>/dev/null < "/proc/$p/cgroup" && [ "$c" = "$me" ]; do
		sleep 0.1; i=$((i + 1))
	done
	[ $i -lt 100 ]
}
desktop_on() {
	[ -e "$S/desktop" ] || return 0
	rm -f "$S/desktop"
	# apps opened from the desktop outlive it. If the instance still runs (a file manager
	# window), this one hands it the --desktop and exits.
	if GDK_GL=disable in_desktop pcmanfm --desktop; then log "leave: PCManFM desktop started"
	else log "pcmanfm --desktop didn't start"; fi
}

procs() {	# the pids in the cgroups of the user units given
	for u in "$@"; do
		cg=$(slice_cg "$u") && find "$cg" -name cgroup.procs -exec cat {} + 2>/dev/null
	done
}
# record: the files the desktop has mapped and the pages of them in memory (ps3-game-prefetch),
# before game mode pushes them out; leave reads them back in one pass. The desktop and the
# display slices' programs, the user manager's (systemd's libraries: systemctl, systemd-run), the
# wallpaper (pcmanfm reads it again) and the programs leave runs.
record() {
	[ -x "$P" ] || return 0
	wp=
	for d in "${XDG_CONFIG_HOME:-$HOME/.config}" $(echo "${XDG_CONFIG_DIRS:-/etc/xdg}" | tr : ' '); do
		f=$d/pcmanfm/default/desktop-items-0.conf
		[ -r "$f" ] && wp=$(sed -n 's/^wallpaper=//p' "$f" | head -1) && break
	done
	set --
	[ -n "$wp" ] && set -- --file "$wp"
	for b in sudo systemctl systemd-run pcmanfm xinput xsetroot udevadm; do
		b=$(command -v $b) && set -- "$@" --file "$b"
	done
	# shellcheck disable=SC2046 # pids
	timeout 30 "$P" record "$S/prefetch" "$@" $(procs ps3-desktop.slice ps3-display.slice init.scope) ||
		rm -f "$S/prefetch"
}

pads() {	# X device ids of the gamepads (udev ID_INPUT_JOYSTICK) attached as pointers
	for d in /sys/class/input/event*; do
		udevadm info -q property "$d" 2>/dev/null | grep -q '^ID_INPUT_JOYSTICK=1' || continue
		name=$(cat "$d/device/name")
		xinput list 2>/dev/null | grep -F "$name" | grep 'slave  pointer' | sed 's/.*id=\([0-9]*\).*/\1/'
	done
}
pads_off() {
	[ -n "${DISPLAY:-}" ] || return 0
	pads > "$S/pads"
	for i in $(cat "$S/pads"); do xinput disable "$i"; done
}
pads_on() {
	[ -r "$S/pads" ] && for i in $(cat "$S/pads"); do xinput enable "$i" 2>/dev/null; done
	rm -f "$S/pads"
}

# proc PID: "<state> <start time>" of a process (fields 3 and 22 of its stat; the name before
# them may hold spaces and parentheses)
proc() {
	read -r st 2>/dev/null < "/proc/$1/stat" || return 1
	st=${st##*) }
	# shellcheck disable=SC2086 # the fields
	set -- $st
	echo "$1 ${20}"
}

alive() {	# alive FILE: one of its processes still runs (a reused pid wouldn't count)
	while read -r p t; do
		[ -n "$p" ] || continue
		if [ -n "$t" ]; then	# a detected game: that very process, not a zombie (its parent
			# may be frozen in ps3-desktop.slice, and reaps it only after leave)
			case $(proc "$p") in Z\ *|X\ *|'') ;; *" $t") return 0 ;; esac
			continue
		fi
		read -r c 2>/dev/null < "/proc/$p/cgroup" || continue
		case $c in *"/ps3-game.slice/"*) return 0 ;; esac
	done < "$1"
	return 1
}

# freeze_desktop CG WAIT: ps3-desktop.slice frozen and pushed out (step 3), once X is free of it
# (x_ready; waited for up to WAIT seconds), and only if X is still free with it frozen. Not now:
# 1, and $S/freeze-pending for run to try again.
freeze_desktop() {
	i=0
	until x_ready "$1"; do
		if [ $i -ge "$2" ]; then
			[ -e "$S/freeze-pending" ] || log "the desktop not frozen yet: $XWHY"
			: > "$S/freeze-pending"; return 1
		fi
		sleep 1; i=$((i + 1))
	done
	freeze "$1" || return 1
	if ! x_ready "$1"; then
		echo 0 > "$1/cgroup.freeze"
		log "the desktop thawed again: frozen, it held X ($XWHY)"
		: > "$S/freeze-pending"; return 1
	fi
	[ -e "$S/freeze-pending" ] && log "the desktop frozen now"
	rm -f "$S/freeze-pending"
	echo "$1" > "$S/frozen"	# for leave: thawing needs no program then
	if [ "${PS3_GAME_PREFETCH:-on}" = keep ]; then
		reclaim_anon "$1"
		cur=$(cat "$1/memory.current" 2>/dev/null) && log "reclaimed ${1##*/} (anonymous only): $((cur / 1024)) kB left"
	else
		reclaim "$1"
	fi
}
freeze_retry() {	# from run, every 3 s for a minute: a freeze deferred by freeze_desktop
	[ -e "$S/freeze-pending" ] && [ ! -e "$S/away" ] && [ $((T % 3)) = 0 ] || return 0
	if [ "$T" -gt 60 ]; then
		rm -f "$S/freeze-pending"
		log "the desktop stays unfrozen for this game: X still held ($XWHY)"
		return 0
	fi
	cg=$(slice_cg ps3-desktop.slice) && freeze_desktop "$cg" 0
}

enter() {
	log "entering game mode (${PS3_GAME_MODE:-on}, prefetch ${PS3_GAME_PREFETCH:-on})"
	pads_off
	[ "${PS3_GAME_MODE:-on}" = pads ] && return
	echo "${PS3_GAME_PREFETCH:-on}" > "$S/prefetch-mode"	# for back
	[ "${PS3_GAME_PREFETCH:-on}" = off ] || record
	desktop_off
	cg=$(slice_cg ps3-desktop.slice)
	if [ -n "$cg" ]; then
		menus_off "$cg"
		freeze_desktop "$cg" 5
	fi
	for u in app.slice session.slice; do
		cg=$(slice_cg $u) && reclaim_anon "$cg"
	done
	: > "$S/root"	# before the call: if the guard dies during it, leave still undoes it
	timeout 90 sudo -n $M on $$ ${PS3_GAME_KICK:-} || log "ps3-game-mode on failed"
}

# leave: what the user sees first. The desktop's files start coming back from the disk
# (ps3-game-prefetch fetch, beside the rest) and the desktop thaws (the cgroup from $S/frozen: no
# program to load from the disk first), then the gamepads and the PCManFM desktop, and the system
# side last. Each step is logged: the journal's times are the timeline.
leave() {
	log "leaving game mode (${SERVICE_RESULT:-}, ${EXIT_CODE:-} ${EXIT_STATUS:-})"
	pf=
	if [ -s "$S/prefetch" ] && [ "${PS3_GAME_PREFETCH:-on}" != off ]; then
		timeout 60 "$P" fetch "$S/prefetch" 9>&- & pf=$!
	fi
	cg=
	[ -e "$S/frozen" ] && read -r cg < "$S/frozen"
	[ -n "$cg" ] || cg=$(slice_cg ps3-desktop.slice)	# not entered this far
	if [ -n "$cg" ] && echo 0 > "$cg/cgroup.freeze" 2>/dev/null && [ -e "$S/frozen" ]; then
		log "leave: desktop thawed"
	fi
	rm -f "$S/frozen" "$S/freeze-pending" "$S/away"
	pads_on
	desktop_on
	if [ -e "$S/root" ]; then
		timeout 60 sudo -n $M off && log "leave: system side done" || log "ps3-game-mode off failed"
	fi
	rm -f "$S/root"
	[ -n "$pf" ] && wait "$pf"
	rm -f "$S/prefetch" "$S/entered" "$S/entering" "$S/leaving" "$S/prefetch-mode"
	rm -rf "$S/rt"
	log "left game mode"
	# the games' records into their lists, with the desktop back (ps3-game-prefetch game-learn)
	set -- "$S"/gp/*.seen
	[ -e "$1" ] && [ -x "$P" ] && timeout 60 "$P" game-learn "$S" 9>&-
	menus_on	# last: its start costs CPU, and nothing needs it at once
	return 0
}

# the games' prefetch (see the top): the watcher started once game mode is on (after
# ps3-game-mode's drop of the page cache), and again for a game no watcher has claimed; stopped
# (it saves its records) before leaving
W=
watch_start() {
	[ "${PS3_GAME_MODE:-on}" != pads ] && [ -x "$P" ] || return 0
	"$P" game-watch "$S" < /dev/null 9>&- & W=$!
}
watch_check() {	# a registered game without a record, and no watcher: one is started
	for f in "$S"/games/*; do
		case $f in *.new) continue ;; esac
		[ -e "$f" ] && [ ! -e "$S/gp/${f##*/}.seen" ] || continue
		if [ -n "$W" ]; then
			case $(proc "$W") in Z\ *|X\ *|'') wait "$W" 2>/dev/null; W= ;; *) return 0 ;; esac
		fi
		watch_start
		return 0
	done
}
watch_stop() {
	[ -n "$W" ] || return 0
	kill -TERM "$W" 2>/dev/null
	{ sleep 30; kill -KILL "$W"; } 2>/dev/null & k=$!
	wait "$W" 2>/dev/null
	kill "$k" 2>/dev/null
	W=
}

# real-time audio (see the top): the registered games' pids to ps3-rt-audio, state in $S/rt
RA=/usr/lib/ps3/desktop/ps3-rt-audio
rt_audio() {
	[ "${PS3_GAME_RT_AUDIO:-on}" != off ] && [ -x "$RA" ] || return 0
	if [ "$T" -le 60 ]; then [ $((T % 2)) = 0 ] || return 0; else [ $((T % 10)) = 0 ] || return 0; fi
	set --
	for f in "$S"/games/*; do
		case $f in *.new) continue ;; esac
		[ -e "$f" ] || continue
		while read -r p _; do [ -n "$p" ] && set -- "$@" "$p"; done < "$f"
	done
	[ $# -gt 0 ] && "$RA" scan "$S/rt" "$@" < /dev/null 9>&-
	return 0
}

# the PS button's escape (see the top), from before entering (a game hanging while it starts)
E=/usr/lib/ps3/desktop/ps3-pad-escape
PE=
pad_escape_start() {
	[ "${PS3_GAME_PAD_ESCAPE:-on}" != off ] && [ -x "$E" ] || return 0
	"$E" < /dev/null 9>&- & PE=$!
}
pad_escape_stop() {
	[ -n "$PE" ] || return 0
	kill -TERM "$PE" 2>/dev/null; wait "$PE" 2>/dev/null; PE=
}

run() {
	mkdir -p "$S/games"
	echo $$ > "$S/guard"
	pad_escape_start
	enter
	watch_start
	exec 9> "$S/lock"
	flock 9; rm -f "$S/entering"; : > "$S/entered"; flock -u 9
	T=0
	while :; do
		sleep 1; T=$((T + 1))
		flock 9
		n=0
		for f in "$S"/games/*; do
			[ -e "$f" ] || continue
			if alive "$f"; then n=$((n + 1)); else rm -f "$f"; fi
		done
		if [ $n = 0 ]; then	# no game left: launchers arriving now wait for leave, then start anew
			rm -f "$S/entered"; : > "$S/leaving"
			flock -u 9
			pad_escape_stop
			watch_stop
			exit 0
		fi
		flock -u 9
		watch_check
		freeze_retry
		rt_audio
	done
}

lock() { exec 9> "$S/lock"; flock 9; }
unlock() { flock -u 9; exec 9>&-; }
live() {	# the guard's service exists (its state files count only then)
	case $(systemctl --user show -P ActiveState ps3-game-mode.service 2>/dev/null) in
	active|activating|deactivating|reloading) return 0 ;;
	esac
	return 1
}
stale() {	# game mode's state left with no guard (one killed with its whole cgroup, whose
	# ExecStopPost was killed too): undone here, by the next join or quit
	if ! live && { [ -e "$S/frozen" ] || [ -e "$S/root" ] || [ -e "$S/pads" ] || [ -e "$S/desktop" ] ||
		[ -e "$S/jgmenu.env" ]; }; then
		log "game mode left behind by a guard that died: undoing it"
		"$0" leave 9>&-	# not the lock's descriptor: the pcmanfm it starts would hold the lock
	fi
}
start_guard() {	# MODE
	env="--setenv=PS3_GAME_MODE=$1"
	for v in DISPLAY XAUTHORITY XDG_CONFIG_DIRS XDG_DATA_DIRS GDK_GL PS3_GAME_PREFETCH PS3_GAME_KICK \
		PS3_GAME_PREFETCH_GAME PS3_GAME_PREFETCH_DIR PS3_GAME_PREFETCH_SAMPLE_S \
		PS3_GAME_PAD_ESCAPE PS3_GAME_PAD_HOLD_S PS3_GAME_PAD_DEVICES PS3_GAME_RT_AUDIO; do
		eval "x=\${$v-}"; [ -n "$x" ] && env="$env --setenv=$v=$x"
	done
	# shellcheck disable=SC2086 # the values have no spaces
	systemd-run --user --unit=ps3-game-mode --slice=ps3-game.slice --collect -q \
		-p TimeoutStopSec=90 -p "ExecStopPost=$0 leave" $env "$0" run
}

join() {	# ID MODE [PID[:START]]...
	me=$S/games/$1 mode=$2
	shift 2
	case $mode in pads) ;; *) mode=on ;; esac
	mkdir -p "$S/games"
	i=0
	while [ $i -lt 150 ]; do	# 30 s
		lock
		if [ ! -e "$me" ]; then
			for p in "$@"; do
				case $p in *:*) echo "${p%%:*} ${p#*:}" ;; *) echo "$p" ;; esac
			done > "$me.new"
			mv "$me.new" "$me"
		fi
		if live; then
			[ -e "$S/entered" ] && { unlock; return 0; }
			# entering (wait for it) or leaving (wait until it is gone, then start anew)
		else
			stale
			rm -f "$S/entered" "$S/entering" "$S/leaving"	# stale: no guard
			: > "$S/entering"
			if ! start_guard "$mode" && ! live; then
				rm -f "$S/entering"; unlock
				log "game mode didn't start"
				return 1
			fi
		fi
		unlock
		sleep 0.2; i=$((i + 1))
	done
	log "game mode not on after 30 s"
	return 1
}

# away / back (see the top), under the lock: run's check of the games and leave's start can't
# come between (back freezes only while the guard's service is active, not once it is leaving)
away() {
	# only a desktop frozen, or to be (not with PS3_GAME_MODE=pads, nor after freeze_retry gave up)
	[ -e "$S/entered" ] && [ ! -e "$S/away" ] && { [ -e "$S/frozen" ] || [ -e "$S/freeze-pending" ]; } ||
		return 0
	: > "$S/away"
	rm -f "$S/freeze-pending"
	[ -e "$S/frozen" ] || { log "away: no game in front (the desktop wasn't frozen yet)"; return 0; }
	read -r cg < "$S/frozen"
	echo 0 > "$cg/cgroup.freeze" 2>/dev/null
	rm -f "$S/frozen"
	log "away: no game in front: the desktop thawed (game mode stays on)"
}
back() {
	[ -e "$S/away" ] || return 0
	rm -f "$S/away"
	[ -e "$S/entered" ] && [ "$(systemctl --user show -P ActiveState ps3-game-mode.service 2>/dev/null)" = active ] ||
		return 0
	log "back: a game in front again"
	read -r PS3_GAME_PREFETCH 2>/dev/null < "$S/prefetch-mode"	# the game's, not the detector's
	cg=$(slice_cg ps3-desktop.slice) && freeze_desktop "$cg" 2
}

case ${1:-} in
run) run ;;
leave) leave ;;
join)	[ $# -ge 3 ] || exit 2
	shift; join "$@" ;;
add)	[ $# = 3 ] || exit 2
	mkdir -p "$S/games"; lock; echo "$3" >> "$S/games/$2"; unlock ;;
quit)	[ $# = 2 ] || exit 2
	mkdir -p "$S/games"; lock; rm -f "$S/games/$2"; stale; unlock ;;
away)	mkdir -p "$S/games"; lock; away; unlock ;;
back)	mkdir -p "$S/games"; lock; back; unlock ;;
*) echo "usage: ps3-game-guard run|leave|join ID MODE [PID[:START]]...|add ID PID|quit ID|away|back" >&2; exit 2 ;;
esac
