mirror of
https://github.com/aydencharles/onionHEN.git
synced 2026-10-19 02:19:47 -05:00
test(lifecycle): cover runtime startup and owner exit handling
This commit is contained in:
1 parent
df92819193
commit
db97d97974
16 files changed
+1205
-87
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 ¤t, 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 ¤t = 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 ¤t = runtime();
|
||||
std::lock_guard<std::mutex> lock(current.mutex);
|
||||
|
||||
@@ -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
|
||||
@@ -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 $@ $<
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user