Graceful GPIO reboot/shutdown (saves srm and metadata) Python functions
-
I've been lurking the forums for a while and wanted to contribute some Python code I put together in order to accomplish GPIO button integration for game reset (or ES restart), graceful reboot, and graceful shutdown. This allows RetroArch to save
.srm
data and allows ES to save metadata properly. I'm leaving out the GPIO-specific code since that will be different for everyone, but I'm successfully using this on an rpi3 with the Kintaro Kuma case, which has a soft power and reset button.Note: This uses RetroArch network commands, so it would need modification to work with non-libretro cores.
Kudos to @meleu for his
killes.sh
script, which I based this on when starting out, and to all those who have contributed to it and other shutdown scripts. Thanks to @cyperghost for his very helpful feedback and contributions as well. Most of the ideas here are not my own, I just collected them together from various forum posts and code snippets into a single Python3 script:#!/usr/bin/python3 -u import errno import os import re import shutil import signal import socket import subprocess import time from pathlib import Path class vars(): pid_timeout = 10 # Timeout PID wait after 10sec sleep_time = 0.2 # Sleep for 1/5sec while polling def touch_file(pathstr): Path(pathstr).touch() shutil.chown(pathstr, "pi", "pi") def get_pid(ex): pid = 0 try: pid_str = subprocess.check_output(['pgrep', '-f', '-o', ex]).strip().decode() pid = int(pid_str) finally: return pid def get_run_pid(): ex = '/opt/retropie/supplementary/runcommand/runcommand.sh' return get_pid(ex) def get_es_pid(): ex = '/opt/retropie/supplementary/.*/emulationstation([^.]|$)' return get_pid(ex) def wait_pid(pid): if pid < 1: return False # Invalid PID wait_counter = 0 hold_time = vars.pid_timeout / vars.sleep_time while True: wait_counter += 1 if wait_counter > hold_time: return False # Timeout try: os.kill(pid, 0) except OSError as err: if err.errno == errno.ESRCH: return True # No such PID time.sleep(vars.sleep_time) def kill_es(): run_pid = get_run_pid() if run_pid: net_command("QUIT") wait_pid(run_pid) es_pid = get_es_pid() if es_pid: os.kill(es_pid, signal.SIGTERM) wait_pid(es_pid) def net_command(command): # RetroArch network command try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("127.0.0.1", 55355)) except socket.error as err: print("Socket error: ", err) else: s.send(command.encode()) finally: if s: s.close def restart_es(): es_pid = get_es_pid() if es_pid: touch_file("/tmp/es-restart") os.kill(es_pid, signal.SIGTERM) def do_reset(): if get_run_pid(): net_command("RESET") else: restart_es() def do_reboot(): kill_es() os.system("sudo reboot") def do_shutdown(): kill_es() os.system("sudo shutdown -h now")
RetroArch network commands need to be enabled, by setting in
/opt/retropie/configs/all/retroarch.cfg
network_cmd_enable = true
Obviously these are all just utility functions, you'll need to wire them up to your GPIO code. Personally, I call
do_shutdown()
for the power switch,do_reset()
for a momentary push and release of the reset switch, anddo_reboot()
after holding down the reset switch.Some findings while writing and testing this:
By default, RetroArch only commits
.srm
data to disk when it gracefully exits. Usingkill
on the process started by runcommand seems to prevent this from happening. Sending a"QUIT"
via network command allows it to exit gracefully (equivalent to the quit hotkey).There is a necessary delay between quitting the emulator core and sending a
kill
signal to ES, otherwise "Terminated." would be printed to the console and metadata would not be saved.I'm guessing that ES needs a bit of time to return control to itself. It might be possible to useruncommand-onend.sh
to get the timing for this more precise, but I haven't tried it. In testing, 1.5 seconds delay afterwait_pid(pid)
was always enough, so I set it to 2 seconds in the example code. The time might need tweaking on other systems.Edit: As @cyperghost pointed out, simply waiting for the
runcommand.sh
PID eliminates the need for asleep
. The code above has been edited to reflect this. Here is the removed function to find the emulator core PID, in case anyone needs it:import linecache def get_core_pid(): run_line = linecache.getline('/dev/shm/runcommand.info', 4) if not run_line: return 0 ex = re.escape(run_line.strip().split(' ', 1)[0]) return get_pid(ex)
When sending a
kill
to ES, it is important to wait for its PID to finish up before issuing ashutdown
orreboot
command in order for metadata to be saved. Thewait
function provided by the OS will not work if the PID you are waiting for is not a child process of the waiting process, so I use thewait_pid(pid)
function instead to do a check in a loop (with a sleep interval and timeout) viaos.kill(pid, 0)
which does not send a kill signal but rather throws aerrno.ESRCH
exception when the PID does not exist.If an emulator is not running when
do_reset()
is called, rather than sending"RESET"
via network command to RetroArch, ES is restarted instead. You could change this if you don't want ES restart functionality. In order to accomplish the restart,/tmp/es-restart
is touched and a kill signal sent to ES. Theemulationstation.sh
script will see the temporary file, remove it, and do the restart for us (equivalent to selecting "Restart EmulationStation" from the ES menu).The
killes.sh
script and shutdown service by @meleu that I referenced while making this, there is a lot of good information in here:ensuring ES gracefully finish and save metadata in every system shutdown
It is still probably the best, most general approach to accomplish clean shutdowns. I'm sharing my code primarily for others who are comfortable editing Python code and already have Python-based GPIO code that they want to integrate clean reboot/shutdowns into directly. @cyperghost wrote a bash script with similar functionality for anyone who would prefer using bash, and there is good info here as well:
posting: Yet annother NESPi case
I hope this is helpful for anyone looking to roll a custom GPIO script for soft buttons!
-
@nachos said in Graceful GPIO reboot/shutdown (saves srm and metadata) Python functions:
It might be possible to use runcommand-onend.sh to get the timing for this more precise, but I haven't tried it.
I think a better way is to process PID from
runcommand.sh
itself. As long as it's running you are not in the ES menu. If this PID isn't available anymore then you can be sure you are back in ES. Then send kill PID to ES. -
@cyperghost Great suggestion, thanks. It would certainly be cleaner. I'll edit the post once I can change the code and test.
-
@nachos No problem ... I used a similar method like your python script in this posting: Yet annother NESPi case.
There are funtions to determinine PIDs and then use a do-loop as long as PID is active. Very similar to yours ....
But I use the PID from the runcommand also and therefore no sleep command is needed.# Power button pressed? if [ $power = 1 ]; then es_action check [[ -n $emupid ]] && kill $emupid && es_wait $emupid && es_wait $rcpid
Function
es_action check
gives feedback of all running processes
Then I check if an emulator is active, if yes ... kill it and wait as long as pid of runcommand is active.EDIT:
Closing words: The shutdown service is imho the best solution. Because you simple do not have to care for the shutdown command itself. Use any coding language you want and initiate a shutdown. And the metadata will always be saved. Believe me without the work @meleu @TMNTturtlguy @cyperghost and much much other helpers and testers this won't be possible ;) -
@Nachos I know this is an old thread, but this is just the thing I have been looking (for a very long time) for!
Please excuse the noob question, but how would I call one of these functions from the command line? Can I use, for example, the quit retroarch function as a bash command or is there another way to call the function?
Any help would be much appreiated!
-
@psb The poster hasn't been on this forum for 1 year and bumping old topics is bad forum etiquette. If you have a question, then open a new topic and ask away - don't re-open years old topics.
Contributions to the project are always appreciated, so if you would like to support us with a donation you can do so here.
Hosting provided by Mythic-Beasts. See the Hosting Information page for more information.