diff --git a/Makefile b/Makefile index 04e9ab5..cfb0464 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VENDORED_LIBS = endif CXXFLAGS = -std=c++11 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp +SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp OBJECTS = $(SOURCES:.cpp=.o) ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o diff --git a/mod/etc/options_menu/save_manager/launch_save_manager.sh b/mod/etc/options_menu/save_manager/launch_save_manager.sh index 33ab81c..238dde9 100644 --- a/mod/etc/options_menu/save_manager/launch_save_manager.sh +++ b/mod/etc/options_menu/save_manager/launch_save_manager.sh @@ -29,11 +29,14 @@ remaining="${OM_BACK_STACK%;*}" command_path="${entry%%,*}" rest="${entry#*,}" script_path="${rest%%,*}" -title_key="${rest#*,}" +title_rest="${rest#*,}" +title_key="${title_rest%%,*}" +marker="" +[ "$title_rest" != "$title_key" ] && marker="${title_rest#*,}" usleep 50000 -if [ -n "$script_path" ]; then - OM_BACK_STACK="$remaining" "$options_path/options" --commandPath "$command_path" --scriptPath "$script_path" --title "$title_key" -else - OM_BACK_STACK="$remaining" "$options_path/options" --commandPath "$command_path" --title "$title_key" -fi +set -- --commandPath "$command_path" +[ -n "$script_path" ] && set -- "$@" --scriptPath "$script_path" +set -- "$@" --title "$title_key" +[ -n "$marker" ] && set -- "$@" --selectedMarker "$marker" +OM_BACK_STACK="$remaining" "$options_path/options" "$@" diff --git a/src/application_lifecycle.h b/src/application_lifecycle.h new file mode 100644 index 0000000..f445aea --- /dev/null +++ b/src/application_lifecycle.h @@ -0,0 +1,97 @@ +// Copyright (C) 2026 DefKorns +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. + +#ifndef APPLICATION_LIFECYCLE_H_ +#define APPLICATION_LIFECYCLE_H_ + +#include +#include +#include +#include +#include +#include + +extern char ** environ; + +enum class ExitAction { Resume, Leave }; + +// Resume runs from the destructor, after main()'s locals (SDL) are released. +class ApplicationLifecycle +{ +public: + ApplicationLifecycle() = default; + ApplicationLifecycle(const ApplicationLifecycle &) = delete; + ApplicationLifecycle & operator=(const ApplicationLifecycle &) = delete; + + ~ApplicationLifecycle() + { + if(action == ExitAction::Resume && !resumed && !script.empty()) + { + resumed = true; + State().resume = 0; + std::system(("/bin/sh " + script).c_str()); + } + } + + void SetResumeScript(std::string path) + { + script = std::move(path); + State().argv[1] = script.c_str(); + } + + void Decide(ExitAction decision) + { + action = decision; + State().resume = decision == ExitAction::Resume ? 1 : 0; + } + + // handler may only call async-signal-safe functions; vfork skips the atfork handlers + void InstallSignalHandlers() + { + struct sigaction sa; + sa.sa_handler = OnTerminate; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGTERM, &sa, nullptr); + sigaction(SIGHUP, &sa, nullptr); + } + +private: + static void OnTerminate(int sig) + { + SignalState & state = State(); + if(!state.fired && state.resume && state.argv[1]) + { + state.fired = 1; + if(vfork() == 0) + { + execve("/bin/sh", const_cast(state.argv), environ); + _exit(127); + } + } + _exit(128 + sig); + } + + struct SignalState + { + const char * argv[3] = { "sh", nullptr, nullptr }; + volatile sig_atomic_t resume = 1; + volatile sig_atomic_t fired = 0; + }; + + static SignalState & State() + { + static SignalState state; + return state; + } + + std::string script; + ExitAction action = ExitAction::Resume; + bool resumed = false; +}; + +#endif diff --git a/src/back_stack.cpp b/src/back_stack.cpp new file mode 100644 index 0000000..60a27db --- /dev/null +++ b/src/back_stack.cpp @@ -0,0 +1,74 @@ +// Copyright (C) 2026 DefKorns +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. + +#include "back_stack.h" + +#include + +namespace back_stack +{ + std::string MakeEntry(const std::string & commandPath, const std::string & scriptPath, + const std::string & titleKey, const std::string & marker) + { + return commandPath + "," + scriptPath + "," + titleKey + (marker.empty() ? "" : "," + marker); + } + + std::string PopIfLast(const std::string & stack, const std::string & entry) + { + const size_t lastSep = stack.find_last_of(';'); + const std::string last = stack.empty() ? "" : (lastSep == std::string::npos ? stack : stack.substr(lastSep + 1)); + if(last != entry) + return stack; + return lastSep == std::string::npos ? "" : stack.substr(0, lastSep); + } + + std::string Push(const std::string & stack, const std::string & entry) + { + return stack.empty() ? entry : stack + ";" + entry; + } + + std::string BackCommand(const std::string & stack, const std::string & optionsLocation) + { + if(stack.empty()) + return ""; + + std::vector entries; + for(size_t start = 0; start < stack.size();) + { + size_t sep = stack.find(';', start); + if(sep == std::string::npos) + sep = stack.size(); + entries.push_back(stack.substr(start, sep - start)); + start = sep + 1; + } + const std::string lastEntry = entries.back(); + entries.pop_back(); + std::string remainingStack; + for(size_t i = 0; i < entries.size(); ++i) + { + if(i) + remainingStack += ";"; + remainingStack += entries[i]; + } + + const size_t c1 = lastEntry.find(','); + const size_t c2 = c1 == std::string::npos ? c1 : lastEntry.find(',', c1 + 1); + if(c2 == std::string::npos) + return ""; + + const std::string backPath = lastEntry.substr(0, c1); + const std::string backScriptPath = lastEntry.substr(c1 + 1, c2 - c1 - 1); + const size_t c3 = lastEntry.find(',', c2 + 1); + const std::string backTitleKey = lastEntry.substr(c2 + 1, c3 == std::string::npos ? std::string::npos : c3 - c2 - 1); + const std::string backMarker = c3 == std::string::npos ? "" : lastEntry.substr(c3 + 1); + + return "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath + + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) + + " --title \"" + backTitleKey + "\"" + + (backMarker.empty() ? "" : " --selectedMarker \"" + backMarker + "\"") + " &"; + } +} diff --git a/src/back_stack.h b/src/back_stack.h new file mode 100644 index 0000000..b5a7c32 --- /dev/null +++ b/src/back_stack.h @@ -0,0 +1,27 @@ +// Copyright (C) 2026 DefKorns +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 3 +// of the License, or (at your option) any later version. + +#ifndef BACK_STACK_H_ +#define BACK_STACK_H_ + +#include + +// OM_BACK_STACK is ';'-separated entries of "commandPath,scriptPath,titleKey[,marker]". +namespace back_stack +{ + std::string MakeEntry(const std::string & commandPath, const std::string & scriptPath, + const std::string & titleKey, const std::string & marker); + + std::string PopIfLast(const std::string & stack, const std::string & entry); + + std::string Push(const std::string & stack, const std::string & entry); + + // empty when the stack is empty or the last entry is malformed + std::string BackCommand(const std::string & stack, const std::string & optionsLocation); +} + +#endif diff --git a/src/main.cpp b/src/main.cpp index fa600e6..c134af1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,8 @@ #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/utf8.h" +#include "application_lifecycle.h" +#include "back_stack.h" #include "command.h" #include "download_screen.h" #include "localization.h" @@ -50,22 +52,14 @@ std::string CollapseSlashes(const std::string & path) return result; } -static struct ExitManager -{ - std::string exitCommand; - bool runExitCommand = true; - ~ExitManager() - { - if(runExitCommand) - system(exitCommand.c_str()); - } -} _exitManager; +static ApplicationLifecycle lifecycle; int main(int argc, char * argv[]) { std::string optionsLocation(argv[0]); optionsLocation = CollapseSlashes(optionsLocation.substr(0, optionsLocation.find_last_of('/')+1)); - _exitManager.exitCommand = "/bin/sh " + optionsLocation + "/scripts/ResumeUI.sh"; + lifecycle.SetResumeScript(optionsLocation + "/scripts/ResumeUI.sh"); + lifecycle.InstallSignalHandlers(); std::string commandLocation(optionsLocation + "commands/"); std::string scriptLocation(optionsLocation + "scripts/"); @@ -113,12 +107,9 @@ int main(int argc, char * argv[]) commandLocation = CollapseSlashes(commandLocation); scriptLocation = CollapseSlashes(scriptLocation); - std::string myEntry = commandLocation + "," + scriptLocation + "," + titleKey + (selectedMarker.empty() ? "" : "," + selectedMarker); - size_t lastSep = backStack.find_last_of(';'); - std::string lastEntry = backStack.empty() ? "" : (lastSep == std::string::npos ? backStack : backStack.substr(lastSep + 1)); - if(lastEntry == myEntry) - backStack = (lastSep == std::string::npos) ? "" : backStack.substr(0, lastSep); - setenv("OM_BACK_STACK", (backStack.empty() ? myEntry : backStack + ";" + myEntry).c_str(), 1); + const std::string myEntry = back_stack::MakeEntry(commandLocation, scriptLocation, titleKey, selectedMarker); + backStack = back_stack::PopIfLast(backStack, myEntry); + setenv("OM_BACK_STACK", back_stack::Push(backStack, myEntry).c_str(), 1); auto ExpandTemplates = [&](std::string & cmd) { @@ -171,51 +162,18 @@ int main(int argc, char * argv[]) else { std::cerr << "Cannot open input folder.\n"; + lifecycle.Decide(ExitAction::Resume); exit(1); } if(commands.empty()) { std::cerr << "No usable commands in " << commandLocation << "\n"; + lifecycle.Decide(ExitAction::Resume); exit(1); } - std::string backCommand; - if(!backStack.empty()) - { - std::vector entries; - for(size_t start = 0; start < backStack.size();) - { - size_t sep = backStack.find(';', start); - if(sep == std::string::npos) sep = backStack.size(); - entries.push_back(backStack.substr(start, sep-start)); - start = sep+1; - } - std::string lastEntry = entries.back(); - entries.pop_back(); - std::string remainingStack; - for(size_t i = 0; i < entries.size(); ++i) - { - if(i) remainingStack += ";"; - remainingStack += entries[i]; - } - - const size_t c1 = lastEntry.find(','); - const size_t c2 = c1 == std::string::npos ? c1 : lastEntry.find(',', c1+1); - if(c2 != std::string::npos) - { - std::string backPath = lastEntry.substr(0, c1); - std::string backScriptPath = lastEntry.substr(c1+1, c2-c1-1); - const size_t c3 = lastEntry.find(',', c2+1); - std::string backTitleKey = lastEntry.substr(c2+1, c3 == std::string::npos ? std::string::npos : c3-c2-1); - std::string backMarker = c3 == std::string::npos ? "" : lastEntry.substr(c3+1); - - backCommand = "usleep 50000 && OM_BACK_STACK=\"" + remainingStack + "\" " + optionsLocation + "options --commandPath " + backPath - + (backScriptPath.empty() ? "" : " --scriptPath " + backScriptPath) - + " --title \"" + backTitleKey + "\"" - + (backMarker.empty() ? "" : " --selectedMarker \"" + backMarker + "\"") + " &"; - } - } + const std::string backCommand = back_stack::BackCommand(backStack, optionsLocation); int currentCommandId = 0; @@ -541,8 +499,8 @@ int main(int argc, char * argv[]) const std::string & confirmKey = commands[currentCommandId].deleteConfirmKey; if(!Confirm(Translate(confirmKey.empty() ? "DELETE_CONFIRM_GENERIC" : confirmKey))) return false; + lifecycle.Decide(ExitAction::Leave); system(commands[currentCommandId].deleteCommand.c_str()); - _exitManager.runExitCommand = false; return true; }; @@ -557,12 +515,14 @@ int main(int argc, char * argv[]) SDL_Event e; while (SDL_PollEvent(&e)) { if (e.type == SDL_QUIT) { + lifecycle.Decide(ExitAction::Resume); return 0; } } if(sdl_context.powerwatch->buttonPress()) { + lifecycle.Decide(ExitAction::Resume); break; } if(controller.GetButtonStatus(A) || controller.GetButtonStatus(START)) @@ -586,8 +546,8 @@ int main(int argc, char * argv[]) } else if(confirmed) { + lifecycle.Decide(commands[currentCommandId].restartUI ? ExitAction::Resume : ExitAction::Leave); system(commands[currentCommandId].command.c_str()); - _exitManager.runExitCommand = commands[currentCommandId].restartUI; break; } } @@ -614,9 +574,9 @@ int main(int argc, char * argv[]) { if(bWasHeld && !bHoldFired) { + lifecycle.Decide(backCommand.empty() ? ExitAction::Resume : ExitAction::Leave); if(!backCommand.empty()) system(backCommand.c_str()); - _exitManager.runExitCommand = backCommand.empty(); break; } bHoldFired = false;