test(lifecycle): cover runtime startup and owner exit handling

This commit is contained in:
aydencharles committed 2026-09-04 13:49:14 +08:00
1 parent df92819193
commit db97d97974
16 files changed
+1205 -87

No files matched your search

+1
View File
@@ -67,6 +67,7 @@ add_subdirectory(libonion_ipc)
add_subdirectory(libonion_settings)
add_subdirectory(libonion_detour)
add_subdirectory(libonion_ready)
add_subdirectory(libonion_lifecycle)
add_subdirectory(libonion_plugin_ui)
add_subdirectory(libonion_plugin_bridge)
add_subdirectory(libonion_plugin_session)
+3 -2
View File
@@ -56,6 +56,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE
"${PROJECT_ROOT}/libonion_ipc/include"
"${PROJECT_ROOT}/libonion_proc/include"
"${PROJECT_ROOT}/libonion_fps/include"
"${PROJECT_ROOT}/libonion_lifecycle/include"
"${PROJECT_ROOT}/libonion_sprx/include"
"${PROJECT_ROOT}/libonion_platform/include"
"${PROJECT_ROOT}/libonion_plugin_session/include"
@@ -65,8 +66,8 @@ target_include_directories(${PROJECT_NAME} PRIVATE
"${PROJECT_ROOT}/include"
)
target_link_directories (${PROJECT_NAME} PUBLIC "${ONIONHEN_PS5_STUB_DIR}")
add_dependencies(${PROJECT_NAME} onion_version util onion_elfldr_server NineS shellui hijacker onion_settings onion_ipc onion_proc onion_fps onion_platform onion_ready onion_plugin_session onion_plugin_bridge onion_plugin_manager onion_elfldr onion_sprx)
add_dependencies(${PROJECT_NAME} onion_version util onion_elfldr_server NineS shellui hijacker onion_settings onion_ipc onion_proc onion_fps onion_platform onion_ready onion_lifecycle onion_plugin_session onion_plugin_bridge onion_plugin_manager onion_elfldr onion_sprx)
# Supervises private :9020 via external :9021, then restarts embedded util via :9020.
target_link_libraries (${PROJECT_NAME} PUBLIC onion_ipc onion_settings onion_proc onion_fps onion_platform onion_ready onion_plugin_session onion_plugin_bridge onion_plugin_manager onion_elfldr onion_sprx hijacker SceSystemService SceNotification SceUserService SceNetCtl SceSysCore kernel_sys NineS)
target_link_libraries (${PROJECT_NAME} PUBLIC onion_ipc onion_settings onion_proc onion_fps onion_platform onion_ready onion_lifecycle onion_plugin_session onion_plugin_bridge onion_plugin_manager onion_elfldr onion_sprx hijacker SceSystemService SceNotification SceUserService SceNetCtl SceSysCore kernel_sys NineS)
onion_strip_debug(${PROJECT_NAME})
@@ -0,0 +1,21 @@
/* Copyright (C) 2026 OnionHEN / LightningMods */
#pragma once
#include <cstdint>
#include <string_view>
#include <sys/types.h>
namespace onion::daemon::app_lifecycle {
/** Start the serial lifecycle dispatcher before the SceSysCore listener. */
bool start();
void stop();
/** Called by the SceSysCore kqueue listener after Big App identification. */
bool publish_big_app_started(pid_t pid, uint32_t app_id,
std::string_view title_id);
bool publish_big_app_exited(pid_t pid, uint32_t app_id,
std::string_view title_id);
} // namespace onion::daemon::app_lifecycle
+17 -1
View File
@@ -5,6 +5,7 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <string>
#include <sys/types.h>
#include <msg.hpp>
@@ -90,9 +91,24 @@ bool Get_Running_App_TID(std::string &title_id, int &BigAppid);
bool Open_Utility_Elf(const char *path, uint8_t **buffer);
/* ---- background threads ---- */
/** Update the app-jailbreak gate and wake the event listener to rebuild. */
/** Update the app-jailbreak gate and wake its runtime to rebuild. */
void app_jailbreak_set_enabled(bool enabled);
/**
* Run Big App lifecycle work on the SceSysCore kqueue owner thread.
* These calls synchronously wait for the listener to acknowledge the work.
*/
bool app_jailbreak_on_big_app_started(pid_t pid, uint32_t app_id,
const char *title_id);
bool app_jailbreak_on_big_app_exited(pid_t pid, uint32_t app_id,
const char *title_id);
/** Start the AppJailbreak runtime and wait until its control watch is ready. */
bool app_jailbreak_runtime_start(pthread_t *thread);
/** Dedicated SceSysCore listener; publishes Big App lifecycle events only. */
void *app_lifecycle_listener_thread(void *args) noexcept;
/** AppJailbreak runtime; owns PID and sandbox vnode monitoring. */
void *fifo_and_dumper_thread(void *args) noexcept; // daemon_jailbreak.cpp
/** Wake the lifecycle listener so it can stop before daemon teardown. */
void app_lifecycle_listener_stop();
void *runtime_supervisor_thread(void *args) noexcept;
/** PID owned by the current util readiness marker, or -1. */
pid_t runtime_owned_util_pid();
@@ -1,5 +1,9 @@
#pragma once
#include <cstdint>
#include <string_view>
#include <sys/types.h>
namespace onion::daemon::sprx_plugins {
inline constexpr const char *kCatalogPath =
@@ -7,6 +11,10 @@ inline constexpr const char *kCatalogPath =
void start();
void reconcile();
void on_big_app_started(pid_t pid, uint32_t app_id,
std::string_view title_id);
void on_big_app_exited(pid_t pid, uint32_t app_id,
std::string_view title_id);
void stop();
} // namespace onion::daemon::sprx_plugins
@@ -0,0 +1,115 @@
/* Copyright (C) 2026 OnionHEN / LightningMods */
#include "app_lifecycle_runtime.hpp"
#include "daemon_ops.hpp"
#include "sprx_plugin_manager_runtime.hpp"
#include <onion/app_lifecycle.hpp>
#include <onion/log.h>
namespace onion::daemon::app_lifecycle {
namespace {
class AppJailbreakLifecycleSubscriber final
: public onion::lifecycle::ISubscriber {
public:
int priority() const noexcept override { return 10; }
bool on_event(const onion::lifecycle::Event &event) noexcept override {
const char *title_id = event.app.title_id.c_str();
bool completed = false;
switch (event.type) {
case onion::lifecycle::EventType::BigAppStarted:
completed = app_jailbreak_on_big_app_started(
event.app.pid, event.app.app_id, title_id);
break;
case onion::lifecycle::EventType::BigAppExited:
completed = app_jailbreak_on_big_app_exited(
event.app.pid, event.app.app_id, title_id);
break;
}
if (!completed) {
LOG_WARN("[lifecycle] AppJailbreak did not acknowledge event type=%u "
"pid=%d tid=%s",
static_cast<unsigned>(event.type), static_cast<int>(event.app.pid),
title_id);
}
return completed;
}
};
class SprxLifecycleSubscriber final : public onion::lifecycle::ISubscriber {
public:
int priority() const noexcept override { return 100; }
bool on_event(const onion::lifecycle::Event &event) noexcept override {
switch (event.type) {
case onion::lifecycle::EventType::BigAppStarted:
sprx_plugins::on_big_app_started(event.app.pid, event.app.app_id,
event.app.title_id);
break;
case onion::lifecycle::EventType::BigAppExited:
sprx_plugins::on_big_app_exited(event.app.pid, event.app.app_id,
event.app.title_id);
break;
}
return true;
}
};
struct Runtime {
AppJailbreakLifecycleSubscriber app_jailbreak;
SprxLifecycleSubscriber sprx;
onion::lifecycle::Dispatcher dispatcher;
Runtime() : dispatcher(32) {
(void)dispatcher.subscribe(app_jailbreak);
(void)dispatcher.subscribe(sprx);
}
};
Runtime &runtime() {
static Runtime value;
return value;
}
bool publish(onion::lifecycle::EventType type, pid_t pid, uint32_t app_id,
std::string_view title_id) {
onion::lifecycle::Event event;
event.type = type;
event.app.pid = pid;
event.app.app_id = app_id;
event.app.title_id = title_id;
if (!runtime().dispatcher.publish(std::move(event))) {
LOG_WARN("[lifecycle] dropped event type=%u pid=%d tid=%.*s",
static_cast<unsigned>(type), static_cast<int>(pid),
static_cast<int>(title_id.size()), title_id.data());
return false;
}
return true;
}
} // namespace
bool start() {
const bool started = runtime().dispatcher.start();
if (!started)
LOG_ERROR("[lifecycle] dispatcher start failed");
return started;
}
void stop() { runtime().dispatcher.stop(); }
bool publish_big_app_started(pid_t pid, uint32_t app_id,
std::string_view title_id) {
return publish(onion::lifecycle::EventType::BigAppStarted, pid, app_id,
title_id);
}
bool publish_big_app_exited(pid_t pid, uint32_t app_id,
std::string_view title_id) {
return publish(onion::lifecycle::EventType::BigAppExited, pid, app_id,
title_id);
}
} // namespace onion::daemon::app_lifecycle
+3
View File
@@ -3,6 +3,7 @@
#include "daemon_ops.hpp"
#include "plugin_ipc_server.hpp"
#include "plugin_manager_runtime.hpp"
#include "app_lifecycle_runtime.hpp"
#include "sprx_plugin_manager_runtime.hpp"
#include <onion/platform.h>
#include <onion/proc_query.h>
@@ -328,6 +329,8 @@ static void shutdown_restart_shellui(void) {
g_stack_shutting_down.store(true, std::memory_order_release);
is_handler_enabled = false;
onion::daemon::plugins::stop();
onion::daemon::app_lifecycle::stop();
app_lifecycle_listener_stop();
onion::daemon::sprx_plugins::stop();
onion::daemon::plugin_ipc::stop();
app_jailbreak_set_enabled(false);
+500 -79
View File
@@ -18,21 +18,28 @@
*/
#include "daemon_ops.hpp"
#include "app_lifecycle_runtime.hpp"
#include "globalconf.hpp"
#include <onion/app_jailbreak_policy.hpp>
#include <onion/app_lifecycle.hpp>
#include <onion/platform.h>
#include <onion/proc_query.h>
#include <onion/settings.hpp>
#include "onion_cjson.hpp"
#include <atomic>
#include <chrono>
#include <cerrno>
#include <climits>
#include <condition_variable>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <deque>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <vector>
@@ -64,6 +71,53 @@ pthread_mutex_t jb_control_lock = PTHREAD_MUTEX_INITIALIZER;
std::atomic_bool jb_listener_enabled{false};
int jb_control_write_fd = -1;
pthread_mutex_t lifecycle_control_lock = PTHREAD_MUTEX_INITIALIZER;
int lifecycle_control_write_fd = -1;
enum class LifecycleListenerControlMessage : char {
Stop = 'S',
};
enum class ControlMessage : char {
Rebuild = 'R',
Lifecycle = 'L',
};
enum class AppJailbreakCommandType {
BigAppStarted,
BigAppExited,
};
struct AppJailbreakCommand {
AppJailbreakCommandType type;
pid_t pid;
uint32_t app_id;
std::string title_id;
std::mutex completion_mutex;
std::condition_variable completion_ready;
bool completed = false;
bool success = false;
};
struct ControlMessages {
bool rebuild_requested = false;
bool lifecycle_requested = false;
};
std::mutex app_jailbreak_commands_mutex;
std::deque<std::shared_ptr<AppJailbreakCommand>> app_jailbreak_commands;
constexpr auto kAppJailbreakCommandTimeout = std::chrono::seconds(2);
constexpr auto kAppJailbreakStartupTimeout = std::chrono::seconds(2);
onion::lifecycle::RuntimeStartupGate app_jailbreak_startup;
std::atomic_bool app_jailbreak_start_cancelled{false};
bool has_pending_app_jailbreak_commands() {
std::lock_guard<std::mutex> lock(app_jailbreak_commands_mutex);
return !app_jailbreak_commands.empty();
}
/** Same authid etaHEN / Hijacker::jailbreak historically used. */
constexpr uint64_t kJbAuthId = 0x4801000000000013ull;
constexpr size_t kMaxRequestBytes = 4096;
@@ -256,12 +310,181 @@ struct TrackedApp {
std::set<pid_t> pids;
};
class JailbreakEventLoop {
struct TrackedBigApp {
uint32_t app_id = 0;
std::string title_id;
};
/** Identifies foreground Big Apps and only publishes lifecycle events. */
class SceSysCoreAppLifecycleCollector {
public:
explicit JailbreakEventLoop(int control_read_fd)
bool on_exec(pid_t pid, const ExecIdentity &id) {
if (id.app_info_rc != 0 || id.tid.empty() || big_apps_.count(pid) != 0)
return false;
std::string current_title_id;
int current_app_id = -1;
if (!Get_Running_App_TID(current_title_id, current_app_id) ||
current_app_id < 0 ||
id.info.app_id != static_cast<uint32_t>(current_app_id) ||
id.tid != current_title_id || get_game_pid() != pid)
return false;
if (!onion::daemon::app_lifecycle::publish_big_app_started(
pid, id.info.app_id, id.tid))
return false;
big_apps_[pid] = {id.info.app_id, id.tid};
LOG_INFO("[lifecycle] Big App started pid=%d appid=%u tid=%s",
static_cast<int>(pid), id.info.app_id, id.tid.c_str());
return true;
}
bool on_exit(pid_t pid) {
const auto found = big_apps_.find(pid);
if (found == big_apps_.end())
return false;
const TrackedBigApp app = found->second;
big_apps_.erase(found);
if (!onion::daemon::app_lifecycle::publish_big_app_exited(
pid, app.app_id, app.title_id))
return false;
LOG_INFO("[lifecycle] Big App exited pid=%d appid=%u tid=%s",
static_cast<int>(pid), app.app_id, app.title_id.c_str());
return true;
}
private:
std::map<pid_t, TrackedBigApp> big_apps_;
};
/** The sole owner of SceSysCore NOTE_EXEC/NOTE_EXIT subscription. */
class SceSysCoreAppLifecycleListener {
public:
explicit SceSysCoreAppLifecycleListener(int control_read_fd)
: control_read_fd_(control_read_fd) {}
~JailbreakEventLoop() {
~SceSysCoreAppLifecycleListener() {
if (kq_ >= 0)
close(kq_);
}
void run() {
if (!start())
return;
struct kevent events[16];
while (!g_stack_shutting_down.load(std::memory_order_acquire)) {
const int count = kevent(kq_, nullptr, 0, events,
sizeof(events) / sizeof(events[0]), nullptr);
if (count < 0) {
if (errno == EINTR)
continue;
LOG_ERROR("[lifecycle] SceSysCore kevent wait failed: %s",
strerror(errno));
return;
}
for (int index = 0; index < count; ++index) {
const struct kevent &event = events[index];
if (event.filter == EVFILT_READ &&
event.ident == static_cast<uintptr_t>(control_read_fd_)) {
drain_control_pipe();
return;
}
if (event.flags & EV_ERROR) {
LOG_ERROR("[lifecycle] SceSysCore event error filter=%d ident=%lu "
"error=%lld",
static_cast<int>(event.filter),
static_cast<unsigned long>(event.ident),
static_cast<long long>(event.data));
continue;
}
if (event.filter == EVFILT_PROC && !handle_process_event(event))
return;
}
}
}
private:
bool start() {
kq_ = kqueue();
if (kq_ < 0) {
LOG_ERROR("[lifecycle] kqueue create failed: %s", strerror(errno));
return false;
}
struct kevent event;
EV_SET(&event, static_cast<uintptr_t>(control_read_fd_), EVFILT_READ,
EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, nullptr);
if (kevent(kq_, &event, 1, nullptr, 0, nullptr) != 0) {
LOG_ERROR("[lifecycle] control-pipe watch failed: %s", strerror(errno));
return false;
}
syscore_pid_ = onion_find_pid("SceSysCore.elf");
if (syscore_pid_ <= 0) {
LOG_ERROR("[lifecycle] cannot find SceSysCore.elf; listener inactive");
return false;
}
EV_SET(&event, static_cast<uintptr_t>(syscore_pid_), EVFILT_PROC,
EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, nullptr);
if (kevent(kq_, &event, 1, nullptr, 0, nullptr) != 0) {
LOG_ERROR("[lifecycle] cannot watch SceSysCore pid=%d: %s",
static_cast<int>(syscore_pid_), strerror(errno));
return false;
}
LOG_INFO("[lifecycle] SceSysCore listener active pid=%d",
static_cast<int>(syscore_pid_));
return true;
}
void drain_control_pipe() const {
char buffer[64];
while (read(control_read_fd_, buffer, sizeof(buffer)) > 0) {
}
}
bool handle_process_event(const struct kevent &event) {
const pid_t pid = static_cast<pid_t>(event.ident);
if (event.fflags & NOTE_EXEC) {
const ExecIdentity id = read_exec_identity(pid);
if (identity_is_shellui(id)) {
LOG_DEBUG("[lifecycle] SysCore EXEC SceShellUI pid=%d",
static_cast<int>(pid));
toolbox_on_new_shellui(pid);
}
(void)collector_.on_exec(pid, id);
}
if (event.fflags & NOTE_EXIT) {
if (onion::lifecycle::ProcessExitPolicy::should_stop_listener(
syscore_pid_, pid)) {
LOG_ERROR("[lifecycle] SceSysCore exited; listener stopped");
return false;
}
(void)collector_.on_exit(pid);
}
return true;
}
int control_read_fd_ = -1;
int kq_ = -1;
pid_t syscore_pid_ = -1;
SceSysCoreAppLifecycleCollector collector_;
};
/** Owns AppJailbreak's PID and sandbox vnode monitoring state. */
class AppJailbreakRuntime {
public:
explicit AppJailbreakRuntime(int control_read_fd)
: control_read_fd_(control_read_fd) {}
~AppJailbreakRuntime() {
close_vnode_watches();
if (kq_ >= 0) {
close(kq_);
@@ -269,12 +492,9 @@ public:
}
void run() {
if (!rebuild()) {
return;
}
struct kevent events[16];
while (!g_stack_shutting_down.load(std::memory_order_acquire)) {
while (!g_stack_shutting_down.load(std::memory_order_acquire) &&
!app_jailbreak_start_cancelled.load(std::memory_order_acquire)) {
const int count = kevent(kq_, nullptr, 0, events,
sizeof(events) / sizeof(events[0]), nullptr);
if (count < 0) {
@@ -289,15 +509,25 @@ public:
const struct kevent &event = events[i];
if (event.filter == EVFILT_READ &&
event.ident == static_cast<uintptr_t>(control_read_fd_)) {
drain_control_pipe();
if (g_stack_shutting_down.load(std::memory_order_acquire)) {
const ControlMessages messages = drain_control_pipe();
// EAGAIN on the writer means a prior wake is pending, which may be
// a rebuild byte rather than a lifecycle byte. Check the queue too.
if (messages.lifecycle_requested ||
has_pending_app_jailbreak_commands()) {
process_app_jailbreak_commands();
}
if (g_stack_shutting_down.load(std::memory_order_acquire) ||
app_jailbreak_start_cancelled.load(std::memory_order_acquire)) {
return;
}
/* Replacing kqueue invalidates every remaining event in this batch. */
if (!rebuild()) {
return;
if (messages.rebuild_requested) {
/* Replacing kqueue invalidates every remaining event in this batch. */
if (!rebuild()) {
return;
}
break;
}
break;
continue;
}
if (event.flags & EV_ERROR) {
@@ -309,7 +539,7 @@ public:
}
if (event.filter == EVFILT_PROC) {
handle_process_event(event);
handle_tracked_process_event(event);
} else if (event.filter == EVFILT_VNODE) {
handle_vnode_event(event);
}
@@ -317,6 +547,8 @@ public:
}
}
bool initialize() { return rebuild(); }
private:
bool install_control_watch(int kq) const {
struct kevent event;
@@ -342,7 +574,6 @@ private:
close(kq_);
}
kq_ = new_kq;
syscore_pid_ = -1;
pid_to_tid_.clear();
active_apps_.clear();
settings_ = g_settings.snapshot();
@@ -351,74 +582,93 @@ private:
jb_listener_enabled.load(std::memory_order_acquire) &&
settings_.app_jailbreak_enabled;
syscore_pid_ = onion_find_pid("SceSysCore.elf");
if (syscore_pid_ <= 0) {
LOG_ERROR("[JB] cannot find SceSysCore.elf; lifecycle listener inactive");
return true;
}
struct kevent event;
EV_SET(&event, static_cast<uintptr_t>(syscore_pid_), EVFILT_PROC,
EV_ADD | EV_ENABLE | EV_CLEAR,
NOTE_FORK | NOTE_EXEC | NOTE_TRACK, 0, nullptr);
if (kevent(kq_, &event, 1, nullptr, 0, nullptr) != 0) {
LOG_ERROR("[JB] cannot watch SceSysCore pid=%d: %s",
static_cast<int>(syscore_pid_), strerror(errno));
syscore_pid_ = -1;
return true;
}
LOG_INFO("[JB] event listener active: SceSysCore pid=%d, "
"fflags=NOTE_FORK|NOTE_EXEC|NOTE_TRACK, jailbreak=%s",
static_cast<int>(syscore_pid_), jb_enabled ? "on" : "off");
LOG_INFO("[JB] runtime active: PID/vnode monitoring, jailbreak=%s",
jb_enabled ? "on" : "off");
return true;
}
void drain_control_pipe() const {
ControlMessages drain_control_pipe() const {
ControlMessages messages;
char buffer[64];
while (read(control_read_fd_, buffer, sizeof(buffer)) > 0) {
for (;;) {
const ssize_t count = read(control_read_fd_, buffer, sizeof(buffer));
if (count <= 0) {
break;
}
for (ssize_t index = 0; index < count; ++index) {
switch (static_cast<ControlMessage>(buffer[index])) {
case ControlMessage::Rebuild:
messages.rebuild_requested = true;
break;
case ControlMessage::Lifecycle:
messages.lifecycle_requested = true;
break;
}
}
}
return messages;
}
void handle_process_event(const struct kevent &event) {
void handle_tracked_process_event(const struct kevent &event) {
const pid_t pid = static_cast<pid_t>(event.ident);
LOG_TRACE("[JB][diag] proc event pid=%d fflags=0x%x data=%lld flags=0x%x",
static_cast<int>(pid), event.fflags,
static_cast<long long>(event.data), event.flags);
if (event.fflags & NOTE_TRACKERR) {
LOG_ERROR("[JB][diag] NOTE_TRACKERR pid=%d parent=%lld; no fallback",
static_cast<int>(pid), static_cast<long long>(event.data));
}
if (event.fflags & NOTE_EXEC) {
const ExecIdentity id = read_exec_identity(pid);
if (identity_is_shellui(id)) {
LOG_DEBUG("[JB] SysCore EXEC SceShellUI pid=%d name='%s' tid='%s'",
static_cast<int>(pid), id.process_name, id.tid.c_str());
toolbox_on_new_shellui(pid);
}
const bool jb_enabled =
jb_listener_enabled.load(std::memory_order_acquire) &&
settings_.app_jailbreak_enabled;
if (jb_enabled) {
inspect_app_process(pid, "exec", id);
}
}
if (event.fflags & NOTE_EXIT) {
if (pid == syscore_pid_) {
LOG_ERROR("[JB] SceSysCore exited; lifecycle listener inactive");
syscore_pid_ = -1;
} else {
remove_app_process(pid);
}
remove_app_process(pid);
}
}
void inspect_app_process(pid_t pid, const char *source,
void process_app_jailbreak_commands() {
for (;;) {
std::shared_ptr<AppJailbreakCommand> command;
{
std::lock_guard<std::mutex> lock(app_jailbreak_commands_mutex);
if (app_jailbreak_commands.empty())
return;
command = std::move(app_jailbreak_commands.front());
app_jailbreak_commands.pop_front();
}
bool success = false;
switch (command->type) {
case AppJailbreakCommandType::BigAppStarted:
success = inspect_big_app_process(command->pid, command->app_id,
command->title_id);
break;
case AppJailbreakCommandType::BigAppExited:
remove_app_process(command->pid);
success = true;
break;
}
{
std::lock_guard<std::mutex> lock(command->completion_mutex);
command->success = success;
command->completed = true;
}
command->completion_ready.notify_one();
}
}
bool inspect_big_app_process(pid_t pid, uint32_t app_id,
const std::string &title_id) {
if (!jb_listener_enabled.load(std::memory_order_acquire) ||
!settings_.app_jailbreak_enabled) {
LOG_DEBUG("[JB] Big App start ignored while AppJailbreak is disabled "
"pid=%d tid=%s",
static_cast<int>(pid), title_id.c_str());
return true;
}
ExecIdentity id;
id.app_info_rc = 0;
id.info.app_id = app_id;
id.tid = title_id;
return inspect_app_process(pid, "big-app", id);
}
bool inspect_app_process(pid_t pid, const char *source,
const ExecIdentity &id) {
if (pid <= 1 || pid_to_tid_.find(pid) != pid_to_tid_.end()) {
return;
return true;
}
if (id.app_info_rc != 0) {
@@ -426,7 +676,7 @@ private:
"GetAppInfo rc=%d errno=%d",
source, static_cast<int>(pid), id.name_rc, id.process_name,
id.app_info_rc, errno);
return;
return false;
}
const std::string &tid = id.tid;
const bool whitelisted =
@@ -439,7 +689,7 @@ private:
onion::app_jailbreak::whitelist_reason(
tid, settings_.app_jailbreak_allowlist));
if (!whitelisted) {
return;
return true;
}
struct kevent event;
@@ -448,7 +698,7 @@ private:
if (kevent(kq_, &event, 1, nullptr, 0, nullptr) != 0) {
LOG_ERROR("[JB] cannot watch exit pid=%d tid=%s: %s",
static_cast<int>(pid), tid.c_str(), strerror(errno));
return;
return false;
}
TrackedApp &app = active_apps_[tid];
@@ -466,6 +716,7 @@ private:
ensure_sandbox_root_watch();
discover_slots(tid);
}
return true;
}
void remove_app_process(pid_t pid) {
@@ -908,7 +1159,6 @@ private:
int control_read_fd_ = -1;
int kq_ = -1;
pid_t syscore_pid_ = -1;
onion::Settings settings_ {};
std::map<int, VnodeWatch> vnode_watches_;
std::map<std::string, int> path_to_fd_;
@@ -916,6 +1166,75 @@ private:
std::map<std::string, TrackedApp> active_apps_;
};
bool enqueue_app_jailbreak_command(AppJailbreakCommandType type, pid_t pid,
uint32_t app_id, const char *title_id) {
if (pid <= 1 || title_id == nullptr || title_id[0] == '\0' ||
g_stack_shutting_down.load(std::memory_order_acquire))
return false;
auto command = std::make_shared<AppJailbreakCommand>();
command->type = type;
command->pid = pid;
command->app_id = app_id;
command->title_id = title_id;
{
std::lock_guard<std::mutex> lock(app_jailbreak_commands_mutex);
app_jailbreak_commands.push_back(command);
}
bool queued_wake = false;
pthread_mutex_lock(&jb_control_lock);
if (jb_control_write_fd >= 0) {
const char wake = static_cast<char>(ControlMessage::Lifecycle);
const ssize_t count = write(jb_control_write_fd, &wake, sizeof(wake));
queued_wake = count == static_cast<ssize_t>(sizeof(wake)) ||
(count < 0 && errno == EAGAIN);
}
pthread_mutex_unlock(&jb_control_lock);
if (!queued_wake) {
std::lock_guard<std::mutex> lock(app_jailbreak_commands_mutex);
for (auto it = app_jailbreak_commands.begin();
it != app_jailbreak_commands.end();
++it) {
if (*it == command) {
app_jailbreak_commands.erase(it);
break;
}
}
LOG_WARN("[lifecycle] cannot wake AppJailbreak listener for pid=%d",
static_cast<int>(pid));
return false;
}
std::unique_lock<std::mutex> lock(command->completion_mutex);
if (!command->completion_ready.wait_for(lock, kAppJailbreakCommandTimeout,
[&command] {
return command->completed;
})) {
LOG_WARN("[lifecycle] AppJailbreak command timed out type=%u pid=%d",
static_cast<unsigned>(type), static_cast<int>(pid));
return false;
}
return command->success;
}
void cancel_pending_app_jailbreak_commands() {
std::deque<std::shared_ptr<AppJailbreakCommand>> pending;
{
std::lock_guard<std::mutex> lock(app_jailbreak_commands_mutex);
pending.swap(app_jailbreak_commands);
}
for (const auto &command : pending) {
{
std::lock_guard<std::mutex> lock(command->completion_mutex);
command->success = false;
command->completed = true;
}
command->completion_ready.notify_one();
}
}
bool set_nonblocking_close_on_exec(int fd) {
const int status_flags = fcntl(fd, F_GETFL, 0);
const int descriptor_flags = fcntl(fd, F_GETFD, 0);
@@ -926,13 +1245,57 @@ bool set_nonblocking_close_on_exec(int fd) {
} // namespace
void app_lifecycle_listener_stop() {
pthread_mutex_lock(&lifecycle_control_lock);
if (lifecycle_control_write_fd >= 0) {
const char wake = static_cast<char>(LifecycleListenerControlMessage::Stop);
const ssize_t ignored =
write(lifecycle_control_write_fd, &wake, sizeof(wake));
(void)ignored;
}
pthread_mutex_unlock(&lifecycle_control_lock);
}
void *app_lifecycle_listener_thread(void *args) noexcept {
(void)args;
int control_pipe[2] = {-1, -1};
if (pipe(control_pipe) != 0 ||
!set_nonblocking_close_on_exec(control_pipe[0]) ||
!set_nonblocking_close_on_exec(control_pipe[1])) {
LOG_ERROR("[lifecycle] control pipe setup failed: %s", strerror(errno));
if (control_pipe[0] >= 0)
close(control_pipe[0]);
if (control_pipe[1] >= 0)
close(control_pipe[1]);
return nullptr;
}
pthread_mutex_lock(&lifecycle_control_lock);
lifecycle_control_write_fd = control_pipe[1];
pthread_mutex_unlock(&lifecycle_control_lock);
{
SceSysCoreAppLifecycleListener listener(control_pipe[0]);
listener.run();
}
pthread_mutex_lock(&lifecycle_control_lock);
if (lifecycle_control_write_fd == control_pipe[1])
lifecycle_control_write_fd = -1;
close(control_pipe[1]);
pthread_mutex_unlock(&lifecycle_control_lock);
close(control_pipe[0]);
LOG_INFO("[lifecycle] SceSysCore listener stopped");
return nullptr;
}
void app_jailbreak_set_enabled(bool enabled) {
const bool previous =
jb_listener_enabled.exchange(enabled, std::memory_order_acq_rel);
pthread_mutex_lock(&jb_control_lock);
if (jb_control_write_fd >= 0) {
const char wake = 1;
const char wake = static_cast<char>(ControlMessage::Rebuild);
const ssize_t ignored = write(jb_control_write_fd, &wake, sizeof(wake));
(void)ignored;
}
@@ -944,6 +1307,18 @@ void app_jailbreak_set_enabled(bool enabled) {
}
}
bool app_jailbreak_on_big_app_started(pid_t pid, uint32_t app_id,
const char *title_id) {
return enqueue_app_jailbreak_command(
AppJailbreakCommandType::BigAppStarted, pid, app_id, title_id);
}
bool app_jailbreak_on_big_app_exited(pid_t pid, uint32_t app_id,
const char *title_id) {
return enqueue_app_jailbreak_command(
AppJailbreakCommandType::BigAppExited, pid, app_id, title_id);
}
void *fifo_and_dumper_thread(void *args) noexcept {
(void)args;
int control_pipe[2] = {-1, -1};
@@ -957,6 +1332,8 @@ void *fifo_and_dumper_thread(void *args) noexcept {
if (control_pipe[1] >= 0) {
close(control_pipe[1]);
}
app_jailbreak_startup.signal(
onion::lifecycle::RuntimeStartupStatus::Failed);
return nullptr;
}
@@ -964,15 +1341,30 @@ void *fifo_and_dumper_thread(void *args) noexcept {
jb_control_write_fd = control_pipe[1];
pthread_mutex_unlock(&jb_control_lock);
LOG_INFO("[JB] worker started (SceSysCore lifecycle + sandbox vnode events)");
if (app_jailbreak_start_cancelled.load(std::memory_order_acquire)) {
app_jailbreak_startup.signal(
onion::lifecycle::RuntimeStartupStatus::Failed);
}
LOG_INFO("[JB] runtime started (PID + sandbox vnode monitoring)");
LOG_DEBUG("[JB] kernel symbols: ALLPROC=0x%lx ROOTVNODE=0x%lx",
static_cast<unsigned long>(KERNEL_ADDRESS_ALLPROC),
static_cast<unsigned long>(KERNEL_ADDRESS_ROOTVNODE));
{
JailbreakEventLoop loop(control_pipe[0]);
loop.run();
AppJailbreakRuntime runtime(control_pipe[0]);
const bool initialized = runtime.initialize();
if (initialized &&
!app_jailbreak_start_cancelled.load(std::memory_order_acquire)) {
app_jailbreak_startup.signal(
onion::lifecycle::RuntimeStartupStatus::Ready);
runtime.run();
} else {
app_jailbreak_startup.signal(
onion::lifecycle::RuntimeStartupStatus::Failed);
}
}
cancel_pending_app_jailbreak_commands();
pthread_mutex_lock(&jb_control_lock);
if (jb_control_write_fd == control_pipe[1]) {
@@ -982,6 +1374,35 @@ void *fifo_and_dumper_thread(void *args) noexcept {
pthread_mutex_unlock(&jb_control_lock);
close(control_pipe[0]);
LOG_INFO("[JB] event listener worker stopped");
LOG_INFO("[JB] runtime stopped");
return nullptr;
}
bool app_jailbreak_runtime_start(pthread_t *thread) {
if (thread == nullptr)
return false;
app_jailbreak_start_cancelled.store(false, std::memory_order_release);
app_jailbreak_startup.reset();
if (pthread_create(thread, nullptr, fifo_and_dumper_thread, nullptr) != 0) {
LOG_ERROR("[JB] runtime thread creation failed");
return false;
}
const auto startup_status = app_jailbreak_startup.wait_for(
std::chrono::duration_cast<std::chrono::milliseconds>(
kAppJailbreakStartupTimeout));
if (startup_status != onion::lifecycle::RuntimeStartupStatus::Ready) {
if (startup_status == onion::lifecycle::RuntimeStartupStatus::Timeout)
LOG_ERROR("[JB] runtime startup timed out");
app_jailbreak_start_cancelled.store(true, std::memory_order_release);
pthread_mutex_lock(&jb_control_lock);
if (jb_control_write_fd >= 0) {
const char wake = static_cast<char>(ControlMessage::Rebuild);
const ssize_t ignored = write(jb_control_write_fd, &wake, sizeof(wake));
(void)ignored;
}
pthread_mutex_unlock(&jb_control_lock);
(void)pthread_join(*thread, nullptr);
return false;
}
return true;
}
+23 -5
View File
@@ -47,6 +47,7 @@ along with this program; see the file COPYING. If not, see
#include "ipc.hpp"
#include "plugin_ipc_server.hpp"
#include "plugin_manager_runtime.hpp"
#include "app_lifecycle_runtime.hpp"
#include "sprx_plugin_manager_runtime.hpp"
#include "startup_navigation.hpp"
#include "welcome_toast.hpp"
@@ -147,8 +148,20 @@ void install_crash_handlers() {
sigaction(i, &action, nullptr);
}
void start_worker_threads(pthread_t* fifo_thr, pthread_t* msg_thr) {
pthread_create(fifo_thr, nullptr, fifo_and_dumper_thread, nullptr);
void start_worker_threads(pthread_t* app_jailbreak_thr, pthread_t* msg_thr) {
if (app_jailbreak_runtime_start(app_jailbreak_thr)) {
pthread_t lifecycle_thr = nullptr;
if (pthread_create(&lifecycle_thr, nullptr, app_lifecycle_listener_thread,
nullptr) == 0) {
pthread_detach(lifecycle_thr);
} else {
LOG_ERROR("[lifecycle] SceSysCore listener thread creation failed");
onion::daemon::app_lifecycle::stop();
}
} else {
LOG_ERROR("[lifecycle] Big App listener disabled: AppJailbreak runtime is "
"not ready");
}
pthread_create(msg_thr, nullptr, IPC_loop, nullptr);
if (!onion::daemon::plugin_ipc::start())
LOG_ERROR("plugin IPC server failed to start");
@@ -247,7 +260,7 @@ int main() {
sceKernelSendNotificationRequest));
char buz[255];
pthread_t fifo_thr = nullptr;
pthread_t app_jailbreak_thr = nullptr;
pthread_t msg_thr = nullptr;
sceNetCtlInit();
@@ -288,7 +301,13 @@ int main() {
[]() -> int { return sceSystemServiceGetAppIdOfRunningBigApp(); });
(void)onion_net_get_ip_address(&buz[0], sizeof(buz));
start_worker_threads(&fifo_thr, &msg_thr);
if (!onion::daemon::app_lifecycle::start())
LOG_ERROR("[lifecycle] startup failed; Big App SPRX events disabled");
// Load the catalog before the SceSysCore listener can publish an event.
// Big App lifecycle events then use reconcile() against this initialized
// catalog, rather than racing a second initial start().
onion::daemon::sprx_plugins::start();
start_worker_threads(&app_jailbreak_thr, &msg_thr);
for (int attempt = 0;
attempt < 20 && !onion::daemon::plugin_ipc::is_listening(); ++attempt)
usleep(50 * 1000);
@@ -296,7 +315,6 @@ int main() {
onion::daemon::plugins::start();
else
LOG_ERROR("[plugins] startup skipped because plugin IPC is not listening");
onion::daemon::sprx_plugins::start();
onion_ready_signal_pid(ONION_READY_DAEMON, getpid());
LOG_DEBUG("is toolbox only: %s | ver: %x", toolbox_only ? "Yes" : "No",
@@ -102,10 +102,46 @@ void run(const char *operation, bool reload) {
log_report(operation, report);
}
bool is_current_target(Runtime &current, pid_t pid, uint32_t app_id,
std::string_view title_id) {
onion::sprx::SprxTarget target;
if (!current.target.current(&target)) {
LOG_DEBUG("[sprx-manager] lifecycle target unavailable pid=%d",
static_cast<int>(pid));
return false;
}
if (target.pid != pid || target.title_id != title_id) {
LOG_DEBUG("[sprx-manager] stale Big App start pid=%d tid=%.*s current_pid=%d current_tid=%s",
static_cast<int>(pid), static_cast<int>(title_id.size()),
title_id.data(), static_cast<int>(target.pid),
target.title_id.c_str());
return false;
}
(void)app_id;
return true;
}
} // namespace
void start() { run("startup", true); }
void reconcile() { run("reconcile", false); }
void on_big_app_started(pid_t pid, uint32_t app_id,
std::string_view title_id) {
Runtime &current = runtime();
std::lock_guard<std::mutex> lock(current.mutex);
if (!is_current_target(current, pid, app_id, title_id))
return;
log_report("big-app-start", current.manager.reconcile());
}
void on_big_app_exited(pid_t pid, uint32_t app_id,
std::string_view title_id) {
LOG_INFO("[sprx-manager] target exited pid=%d appid=%u tid=%.*s; no daemon-owned SPRX runtime or UI session to release",
static_cast<int>(pid), app_id, static_cast<int>(title_id.size()),
title_id.data());
}
void stop() {
Runtime &current = runtime();
std::lock_guard<std::mutex> lock(current.mutex);
+16
View File
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.20)
set(basename "onion_lifecycle")
project(${basename} CXX)
onion_module_prologue(CXX_STANDARD 20)
set(D_CWD "${PROJECT_SOURCE_DIR}")
add_library(${PROJECT_NAME} STATIC source/app_lifecycle.cpp)
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}")
set(CMAKE_CXX_FLAGS "${ONION_PS5_TARGET_FLAGS_MARCH} -fPIC -fstack-protector-all")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -Wall -Wextra -Werror -fno-exceptions")
target_include_directories(${PROJECT_NAME}
PUBLIC "${D_CWD}/include"
)
@@ -0,0 +1,115 @@
/* Copyright (C) 2026 OnionHEN / LightningMods */
#pragma once
#include <cstddef>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <pthread.h>
#include <string>
#include <vector>
#include <sys/types.h>
namespace onion::lifecycle {
enum class RuntimeStartupStatus : uint8_t {
Pending,
Ready,
Failed,
Timeout,
};
/** Thread-safe startup handshake shared by a runtime owner and its starter. */
class RuntimeStartupGate final {
public:
RuntimeStartupGate() = default;
RuntimeStartupGate(const RuntimeStartupGate &) = delete;
RuntimeStartupGate &operator=(const RuntimeStartupGate &) = delete;
void reset() noexcept;
void signal(RuntimeStartupStatus status) noexcept;
RuntimeStartupStatus wait_for(std::chrono::milliseconds timeout) noexcept;
RuntimeStartupStatus status() const noexcept;
private:
mutable std::mutex mutex_;
std::condition_variable ready_;
RuntimeStartupStatus status_ = RuntimeStartupStatus::Pending;
};
/** Keeps the owner process exit from being mistaken for an app exit. */
class ProcessExitPolicy final {
public:
static bool should_stop_listener(pid_t owner_pid, pid_t event_pid) noexcept {
return owner_pid > 1 && owner_pid == event_pid;
}
};
enum class EventType : uint8_t {
BigAppStarted,
BigAppExited,
};
struct BigApp {
pid_t pid = -1;
uint32_t app_id = 0;
std::string title_id;
};
struct Event {
EventType type = EventType::BigAppStarted;
BigApp app;
uint64_t sequence = 0;
};
/**
* A lifecycle callback executes on the dispatcher's one worker thread.
* Lower priorities run first; equal priorities preserve subscription order.
*/
class ISubscriber {
public:
virtual ~ISubscriber() = default;
virtual int priority() const noexcept = 0;
/** Return false to stop dispatching this event to lower-priority subscribers. */
virtual bool on_event(const Event &event) noexcept = 0;
};
/**
* Bounded, FIFO lifecycle dispatcher. Event collection may happen on a
* kqueue thread, but subscribers are always invoked serially on this worker.
* Subscriptions are immutable after start to keep ordering deterministic.
*/
class Dispatcher final {
public:
explicit Dispatcher(size_t max_pending = 32) noexcept;
~Dispatcher();
Dispatcher(const Dispatcher &) = delete;
Dispatcher &operator=(const Dispatcher &) = delete;
bool subscribe(ISubscriber &subscriber) noexcept;
bool start() noexcept;
void stop() noexcept;
bool publish(Event event) noexcept;
private:
static void *worker_entry(void *context) noexcept;
void run() noexcept;
size_t max_pending_;
std::mutex mutex_;
std::condition_variable ready_;
std::vector<ISubscriber *> subscribers_;
std::deque<Event> pending_;
pthread_t worker_ {};
uint64_t next_sequence_ = 1;
bool started_ = false;
bool stopping_ = false;
};
} // namespace onion::lifecycle
@@ -0,0 +1,120 @@
/* Copyright (C) 2026 OnionHEN / LightningMods */
#include <onion/app_lifecycle.hpp>
#include <algorithm>
namespace onion::lifecycle {
void RuntimeStartupGate::reset() noexcept {
std::lock_guard<std::mutex> lock(mutex_);
status_ = RuntimeStartupStatus::Pending;
}
void RuntimeStartupGate::signal(RuntimeStartupStatus status) noexcept {
{
std::lock_guard<std::mutex> lock(mutex_);
if (status_ == RuntimeStartupStatus::Pending)
status_ = status;
}
ready_.notify_all();
}
RuntimeStartupStatus RuntimeStartupGate::wait_for(
std::chrono::milliseconds timeout) noexcept {
std::unique_lock<std::mutex> lock(mutex_);
if (!ready_.wait_for(lock, timeout, [this] {
return status_ != RuntimeStartupStatus::Pending;
}) && status_ == RuntimeStartupStatus::Pending) {
status_ = RuntimeStartupStatus::Timeout;
}
return status_;
}
RuntimeStartupStatus RuntimeStartupGate::status() const noexcept {
std::lock_guard<std::mutex> lock(mutex_);
return status_;
}
Dispatcher::Dispatcher(size_t max_pending) noexcept
: max_pending_(max_pending == 0 ? 1 : max_pending) {}
Dispatcher::~Dispatcher() { stop(); }
bool Dispatcher::subscribe(ISubscriber &subscriber) noexcept {
std::lock_guard<std::mutex> lock(mutex_);
if (started_ || std::find(subscribers_.begin(), subscribers_.end(),
&subscriber) != subscribers_.end())
return false;
subscribers_.push_back(&subscriber);
std::stable_sort(subscribers_.begin(), subscribers_.end(),
[](const ISubscriber *left, const ISubscriber *right) {
return left->priority() < right->priority();
});
return true;
}
bool Dispatcher::start() noexcept {
std::lock_guard<std::mutex> lock(mutex_);
if (started_)
return true;
stopping_ = false;
if (pthread_create(&worker_, nullptr, worker_entry, this) != 0)
return false;
started_ = true;
return true;
}
void Dispatcher::stop() noexcept {
pthread_t worker {};
{
std::lock_guard<std::mutex> lock(mutex_);
if (!started_)
return;
stopping_ = true;
pending_.clear();
worker = worker_;
ready_.notify_one();
}
(void)pthread_join(worker, nullptr);
std::lock_guard<std::mutex> lock(mutex_);
started_ = false;
}
bool Dispatcher::publish(Event event) noexcept {
std::lock_guard<std::mutex> lock(mutex_);
if (!started_ || stopping_ || event.app.pid <= 1 || event.app.title_id.empty() ||
pending_.size() >= max_pending_)
return false;
event.sequence = next_sequence_++;
pending_.push_back(std::move(event));
ready_.notify_one();
return true;
}
void *Dispatcher::worker_entry(void *context) noexcept {
static_cast<Dispatcher *>(context)->run();
return nullptr;
}
void Dispatcher::run() noexcept {
for (;;) {
Event event;
std::vector<ISubscriber *> subscribers;
{
std::unique_lock<std::mutex> lock(mutex_);
ready_.wait(lock, [this] { return stopping_ || !pending_.empty(); });
if (stopping_)
return;
event = std::move(pending_.front());
pending_.pop_front();
subscribers = subscribers_;
}
for (ISubscriber *subscriber : subscribers) {
if (!subscriber->on_event(event))
break;
}
}
}
} // namespace onion::lifecycle
+8
View File
@@ -33,6 +33,7 @@ COMMON_FLAGS := -Wall -Wextra -g -O0 $(DEPFLAGS) -DONION_HOST_TEST=1 -DCBC=1 \
-I$(REPO_ROOT)/source/libonion_detour/include \
-I$(REPO_ROOT)/source/libonion_ready \
-I$(REPO_ROOT)/source/libonion_fps/include \
-I$(REPO_ROOT)/source/libonion_lifecycle/include \
-I$(REPO_ROOT)/source/libonion_sprx/include \
-I$(REPO_ROOT)/source/libonion_plugin_ui/include \
-I$(REPO_ROOT)/source/libonion_plugin_bridge/include \
@@ -98,6 +99,7 @@ PROD_CXX_SRCS := \
$(REPO_ROOT)/source/shellui/src/dynamic_ui_xml.cpp \
$(REPO_ROOT)/source/shellui/src/dynamic_ui_runtime.cpp \
$(REPO_ROOT)/source/libonion_fps/source/fps_formula.cpp \
$(REPO_ROOT)/source/libonion_lifecycle/source/app_lifecycle.cpp \
$(REPO_ROOT)/source/libonion_sprx/source/sprx_loader.cpp \
$(REPO_ROOT)/source/libonion_sprx/source/sprx_catalog.cpp \
$(REPO_ROOT)/source/libonion_sprx/source/sprx_plugin_manager.cpp
@@ -141,6 +143,7 @@ TEST_CXX_SRCS := \
test_overlay_text_metrics.cpp \
test_overlay_layout.cpp \
test_fps_formula.cpp \
test_app_lifecycle.cpp \
test_sprx_loader.cpp \
test_sprx_catalog.cpp \
test_sprx_plugin_manager.cpp \
@@ -179,6 +182,7 @@ PROD_CXX_OBJS += $(BUILD_DIR)/prod/shellui/ps5_settings_ui.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/shellui/toolbox_route.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/shellui/toolbox_i18n.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/libonion_fps/fps_formula.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/libonion_lifecycle/app_lifecycle.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/libonion_sprx/sprx_loader.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/libonion_sprx/sprx_catalog.o
PROD_CXX_OBJS += $(BUILD_DIR)/prod/libonion_sprx/sprx_plugin_manager.o
@@ -320,6 +324,10 @@ $(BUILD_DIR)/prod/libonion_fps/fps_formula.o: $(REPO_ROOT)/source/libonion_fps/s
@mkdir -p $(dir $@)
$(HOST_CXX) $(CXXFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod/libonion_lifecycle/app_lifecycle.o: $(REPO_ROOT)/source/libonion_lifecycle/source/app_lifecycle.cpp | $(BUILD_DIR)
@mkdir -p $(dir $@)
$(HOST_CXX) $(CXXFLAGS) -c -o $@ $<
$(BUILD_DIR)/prod/libonion_sprx/sprx_loader.o: $(REPO_ROOT)/source/libonion_sprx/source/sprx_loader.cpp | $(BUILD_DIR)
@mkdir -p $(dir $@)
$(HOST_CXX) $(CXXFLAGS) -c -o $@ $<
+217
View File
@@ -0,0 +1,217 @@
#include "test_harness.h"
#include <onion/app_lifecycle.hpp>
#include <atomic>
#include <chrono>
#include <mutex>
#include <string>
#include <unistd.h>
#include <vector>
namespace {
struct FakeRuntime {
onion::lifecycle::RuntimeStartupGate startup;
};
void *fake_runtime_ready(void *context) {
auto *runtime = static_cast<FakeRuntime *>(context);
runtime->startup.signal(onion::lifecycle::RuntimeStartupStatus::Ready);
return nullptr;
}
void *fake_runtime_slow_ready(void *context) {
auto *runtime = static_cast<FakeRuntime *>(context);
usleep(20 * 1000);
runtime->startup.signal(onion::lifecycle::RuntimeStartupStatus::Ready);
return nullptr;
}
int test_fake_runtime_startup_handshake() {
FakeRuntime runtime;
pthread_t worker {};
TEST_ASSERT_EQ_INT(0, pthread_create(&worker, nullptr, fake_runtime_ready,
&runtime));
TEST_ASSERT_TRUE(runtime.startup.wait_for(std::chrono::milliseconds(100)) ==
onion::lifecycle::RuntimeStartupStatus::Ready);
TEST_ASSERT_EQ_INT(0, pthread_join(worker, nullptr));
return 0;
}
int test_fake_runtime_timeout_cannot_be_revived() {
FakeRuntime runtime;
pthread_t worker {};
TEST_ASSERT_EQ_INT(0, pthread_create(&worker, nullptr,
fake_runtime_slow_ready, &runtime));
TEST_ASSERT_TRUE(runtime.startup.wait_for(std::chrono::milliseconds(1)) ==
onion::lifecycle::RuntimeStartupStatus::Timeout);
TEST_ASSERT_EQ_INT(0, pthread_join(worker, nullptr));
TEST_ASSERT_TRUE(runtime.startup.status() ==
onion::lifecycle::RuntimeStartupStatus::Timeout);
return 0;
}
int test_runtime_startup_gate_ready_failed_and_timeout() {
onion::lifecycle::RuntimeStartupGate gate;
TEST_ASSERT_TRUE(gate.status() ==
onion::lifecycle::RuntimeStartupStatus::Pending);
gate.signal(onion::lifecycle::RuntimeStartupStatus::Ready);
TEST_ASSERT_TRUE(gate.wait_for(std::chrono::milliseconds(1)) ==
onion::lifecycle::RuntimeStartupStatus::Ready);
gate.reset();
gate.signal(onion::lifecycle::RuntimeStartupStatus::Failed);
TEST_ASSERT_TRUE(gate.wait_for(std::chrono::milliseconds(1)) ==
onion::lifecycle::RuntimeStartupStatus::Failed);
gate.reset();
TEST_ASSERT_TRUE(gate.wait_for(std::chrono::milliseconds(1)) ==
onion::lifecycle::RuntimeStartupStatus::Timeout);
gate.signal(onion::lifecycle::RuntimeStartupStatus::Ready);
TEST_ASSERT_TRUE(gate.status() ==
onion::lifecycle::RuntimeStartupStatus::Timeout);
return 0;
}
int test_fake_syscore_listener_stops_on_owner_exit_only() {
struct FakeListener {
pid_t owner_pid = 100;
bool running = true;
void on_exit(pid_t event_pid) {
if (onion::lifecycle::ProcessExitPolicy::should_stop_listener(
owner_pid, event_pid))
running = false;
}
} listener;
listener.on_exit(101);
TEST_ASSERT_TRUE(listener.running);
listener.on_exit(100);
TEST_ASSERT_TRUE(!listener.running);
TEST_ASSERT_TRUE(!onion::lifecycle::ProcessExitPolicy::should_stop_listener(
-1, 100));
return 0;
}
struct Invocation {
int subscriber_priority = 0;
onion::lifecycle::EventType type;
uint64_t sequence = 0;
};
class Recorder final : public onion::lifecycle::ISubscriber {
public:
explicit Recorder(int value, std::vector<Invocation> &out,
std::mutex &out_mutex, bool should_succeed = true)
: value_(value), out_(out), out_mutex_(out_mutex),
should_succeed_(should_succeed) {}
int priority() const noexcept override { return value_; }
bool on_event(const onion::lifecycle::Event &event) noexcept override {
std::lock_guard<std::mutex> lock(out_mutex_);
out_.push_back({value_, event.type, event.sequence});
return should_succeed_;
}
private:
int value_;
std::vector<Invocation> &out_;
std::mutex &out_mutex_;
bool should_succeed_;
};
bool wait_for_count(const std::vector<Invocation> &events, std::mutex &mutex,
size_t count) {
for (int attempt = 0; attempt < 1000; ++attempt) {
{
std::lock_guard<std::mutex> lock(mutex);
if (events.size() == count)
return true;
}
usleep(1000);
}
return false;
}
int test_dispatches_fifo_by_priority() {
std::vector<Invocation> events;
std::mutex events_mutex;
Recorder later(20, events, events_mutex);
Recorder earlier(10, events, events_mutex);
onion::lifecycle::Dispatcher dispatcher;
TEST_ASSERT_TRUE(dispatcher.subscribe(later));
TEST_ASSERT_TRUE(dispatcher.subscribe(earlier));
TEST_ASSERT_TRUE(!dispatcher.subscribe(earlier));
TEST_ASSERT_TRUE(dispatcher.start());
TEST_ASSERT_TRUE(!dispatcher.subscribe(earlier));
onion::lifecycle::Event started;
started.type = onion::lifecycle::EventType::BigAppStarted;
started.app = {123, 7, "CUSA12345"};
onion::lifecycle::Event exited;
exited.type = onion::lifecycle::EventType::BigAppExited;
exited.app = {123, 7, "CUSA12345"};
TEST_ASSERT_TRUE(dispatcher.publish(std::move(started)));
TEST_ASSERT_TRUE(dispatcher.publish(std::move(exited)));
TEST_ASSERT_TRUE(wait_for_count(events, events_mutex, 4));
dispatcher.stop();
std::lock_guard<std::mutex> lock(events_mutex);
TEST_ASSERT_EQ_INT(4, static_cast<int>(events.size()));
TEST_ASSERT_EQ_INT(10, events[0].subscriber_priority);
TEST_ASSERT_EQ_INT(20, events[1].subscriber_priority);
TEST_ASSERT_EQ_INT(10, events[2].subscriber_priority);
TEST_ASSERT_EQ_INT(20, events[3].subscriber_priority);
TEST_ASSERT_TRUE(events[0].type == onion::lifecycle::EventType::BigAppStarted);
TEST_ASSERT_TRUE(events[2].type == onion::lifecycle::EventType::BigAppExited);
TEST_ASSERT_EQ_U64(1, events[0].sequence);
TEST_ASSERT_EQ_U64(1, events[1].sequence);
TEST_ASSERT_EQ_U64(2, events[2].sequence);
TEST_ASSERT_EQ_U64(2, events[3].sequence);
return 0;
}
int test_stops_lower_priority_subscribers_after_failure() {
std::vector<Invocation> events;
std::mutex events_mutex;
Recorder failing(10, events, events_mutex, false);
Recorder blocked(100, events, events_mutex);
onion::lifecycle::Dispatcher dispatcher;
TEST_ASSERT_TRUE(dispatcher.subscribe(blocked));
TEST_ASSERT_TRUE(dispatcher.subscribe(failing));
TEST_ASSERT_TRUE(dispatcher.start());
onion::lifecycle::Event event;
event.type = onion::lifecycle::EventType::BigAppStarted;
event.app = {123, 7, "CUSA12345"};
TEST_ASSERT_TRUE(dispatcher.publish(std::move(event)));
TEST_ASSERT_TRUE(wait_for_count(events, events_mutex, 1));
dispatcher.stop();
std::lock_guard<std::mutex> lock(events_mutex);
TEST_ASSERT_EQ_INT(1, static_cast<int>(events.size()));
TEST_ASSERT_EQ_INT(10, events[0].subscriber_priority);
return 0;
}
} // namespace
extern "C" int test_app_lifecycle_suite(void) {
int failures = 0;
failures += onion_test_run("app_lifecycle.fake_runtime_handshake",
test_fake_runtime_startup_handshake);
failures += onion_test_run("app_lifecycle.fake_runtime_timeout_cleanup",
test_fake_runtime_timeout_cannot_be_revived);
failures += onion_test_run("app_lifecycle.runtime_startup_gate",
test_runtime_startup_gate_ready_failed_and_timeout);
failures += onion_test_run("app_lifecycle.fake_syscore_owner_exit",
test_fake_syscore_listener_stops_on_owner_exit_only);
failures += onion_test_run("app_lifecycle.serial_priority",
test_dispatches_fifo_by_priority);
failures += onion_test_run("app_lifecycle.failure_stops_chain",
test_stops_lower_priority_subscribers_after_failure);
return failures;
}
+2
View File
@@ -37,6 +37,7 @@ extern "C" int test_toolbox_i18n_suite(void);
extern "C" int test_overlay_text_metrics_suite(void);
extern "C" int test_overlay_layout_suite(void);
extern "C" int test_fps_formula_suite(void);
extern "C" int test_app_lifecycle_suite(void);
extern "C" int test_sprx_loader_suite(void);
extern "C" int test_sprx_catalog_suite(void);
extern "C" int test_sprx_plugin_manager_suite(void);
@@ -83,6 +84,7 @@ int main() {
failures += test_overlay_text_metrics_suite();
failures += test_overlay_layout_suite();
failures += test_fps_formula_suite();
failures += test_app_lifecycle_suite();
failures += test_sprx_loader_suite();
failures += test_sprx_catalog_suite();
failures += test_sprx_plugin_manager_suite();