From 7660b10a7590089dd3649b15582e2bb85ff68acf Mon Sep 17 00:00:00 2001 From: inmert <54280820+inmert@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:11:18 +0530 Subject: [PATCH] feat(libSceSystemService): implement sceSystemServiceLaunchWebBrowser Open the URI in the host's default browser: ShellExecuteW on Windows, xdg-open on Linux and open on macOS. The (uri, param) signature follows the public homebrew callers, which all pass a null param; a non-null param and a scheme other than http or https throw. A null or empty URI returns SYSTEM_SERVICE_ERROR_PARAMETER. PPSA69420 (ScreenTester5 01.000.000) imports it (#1578). --- .../prx/libSceSystemService/CMakeLists.txt | 3 + core/libs/prx/libSceSystemService/Export.cpp | 68 +++++++++++++++++++ core/libs/tests/GuestSystemService.cpp | 26 +++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 98 insertions(+) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index b1676e0f52f..0b12693254a 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,5 +1,8 @@ add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) +if(WIN32) + target_link_libraries(libSceSystemService PRIVATE shell32) +endif() configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 0de0c37f403..66d9762cae8 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,14 +1,82 @@ +#include #include #include #include #include +#include +#include +#include #include "prx/libc/include/Shutdown.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +extern char** environ; +#endif + +namespace { + +bool hasWebScheme(std::string_view uri) { + for (std::string_view scheme : {std::string_view("http://"), std::string_view("https://")}) { + if (uri.size() < scheme.size()) continue; + bool matches = true; + for (std::size_t i = 0; i < scheme.size() && matches; ++i) { + matches = std::tolower(static_cast(uri[i])) == scheme[i]; + } + if (matches) return true; + } + return false; +} + +void openInHostBrowser(const char* uri) { +#ifdef _WIN32 + const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, uri, -1, nullptr, 0); + if (length <= 0) throw std::runtime_error("sceSystemServiceLaunchWebBrowser: the URI is not valid UTF-8"); + std::wstring wide(static_cast(length), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, uri, -1, wide.data(), length); + const auto result = reinterpret_cast(ShellExecuteW(nullptr, L"open", wide.c_str(), nullptr, nullptr, SW_SHOWNORMAL)); + if (result <= 32) { + throw std::runtime_error("sceSystemServiceLaunchWebBrowser: ShellExecuteW failed with " + std::to_string(result)); + } +#else +#ifdef __APPLE__ + const char* opener = "open"; +#else + const char* opener = "xdg-open"; +#endif + char* const arguments[] = {const_cast(opener), const_cast(uri), nullptr}; + pid_t pid = 0; + const int error = posix_spawnp(&pid, opener, nullptr, nullptr, arguments, environ); + if (error != 0) { + throw std::runtime_error(std::string("sceSystemServiceLaunchWebBrowser: cannot start ") + opener + ": " + std::strerror(error)); + } + std::thread([pid] { + int status = 0; + waitpid(pid, &status, 0); + }).detach(); +#endif +} + +} + extern "C" { +int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, const void* param) { + if (uri == nullptr || *uri == '\0') return SYSTEM_SERVICE_ERROR_PARAMETER; + if (param != nullptr) NotImplemented_nid_no_patch("sceSystemServiceLaunchWebBrowser: launch parameter"); + if (!hasWebScheme(uri)) NotImplemented_nid_no_patch("sceSystemServiceLaunchWebBrowser: URI scheme other than http or https"); + openInHostBrowser(uri); + return SYSTEM_SERVICE_OK; +} + + int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; if (std::strcmp(path, "exit") != 0) { diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index 6341c8d4068..35bb553bb87 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -27,8 +27,34 @@ extern "C" int APS5_VABI sceSystemServicePowerTick(void); extern "C" int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info); extern "C" int APS5_VABI sceSystemServiceDisableMusicPlayer(void); extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); +extern "C" int APS5_VABI sceSystemServiceLaunchWebBrowser(const char* uri, const void* param); + +namespace { + +bool LaunchWebBrowserThrows(const char* uri, const void* param, const char* what) { + try { + static_cast(sceSystemServiceLaunchWebBrowser(uri, param)); + } catch (const std::runtime_error& error) { + return std::strcmp(error.what(), what) == 0; + } + return false; +} + +} int main() { + Require(sceSystemServiceLaunchWebBrowser(nullptr, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + Require(sceSystemServiceLaunchWebBrowser("", nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); + int launchParam = 0; + Require(LaunchWebBrowserThrows("https://example.com/", &launchParam, + "sceSystemServiceLaunchWebBrowser: launch parameter not implemented")); + constexpr const char* schemeNotImplemented = + "sceSystemServiceLaunchWebBrowser: URI scheme other than http or https not implemented"; + Require(LaunchWebBrowserThrows("ftp://example.com/", nullptr, schemeNotImplemented)); + Require(LaunchWebBrowserThrows("file:///etc/passwd", nullptr, schemeNotImplemented)); + Require(LaunchWebBrowserThrows("example.com", nullptr, schemeNotImplemented)); + Require(LaunchWebBrowserThrows("http:/example.com", nullptr, schemeNotImplemented)); + Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); Require(sceSystemServiceReportAbnormalTermination(nullptr) == SYSTEM_SERVICE_OK); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 81b67436620..04fbb7c3240 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -196,6 +196,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw +- [sceSystemServiceLaunchWebBrowser](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the `(uri, param)` signature follows the public homebrew callers ([CTurt's PS4 userland write-up](https://cturt.github.io/ps4-2.html), [elf-launcher](https://github.com/X-F1REBALL-X/elf-launcher/blob/main/host/hbinstall/main.c), [stremio-ps4](https://github.com/bielstar123-source/stremio-ps4/blob/main/main.cpp)), which all pass a null `param`; the parameter's layout is unknown, so a non-null one throws. The URI opens in the host's default browser (`ShellExecuteW` on Windows, `xdg-open` on Linux, `open` on macOS) and the call returns at once; only `http` and `https` URIs are passed on, other schemes throw. The console's error for a bad URI, whether the call waits for the browser, and the overlay status and events a title sees while the browser is up are unverified: `sceSystemServiceGetStatus` keeps reporting no overlay and no event is queued. PPSA69420 (ScreenTester5 01.000.000) imports it; no title has been seen calling it - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw