From 540bbf4c974a75c8de085d5c05118eb4529551c4 Mon Sep 17 00:00:00 2001 From: xbzk Date: Sat, 1 Aug 2026 12:35:45 -0300 Subject: [PATCH] [loader] Add nxlink log server mode --- src/common/settings.h | 3 + src/common/settings_enums.h | 1 + src/core/core.cpp | 2 + src/core/loader/homebrew_nxlink.cpp | 204 +++++++++++++++++++++++++++- src/core/loader/homebrew_nxlink.h | 13 +- src/core/loader/nro.cpp | 22 ++- 6 files changed, 236 insertions(+), 9 deletions(-) diff --git a/src/common/settings.h b/src/common/settings.h index 7bcec7e3ba..0e336a05ff 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -840,6 +840,9 @@ struct Values { Setting gpu_log_driver_debug{linkage, true, "gpu_log_driver_debug", Category::Debugging}; Setting gpu_log_ring_buffer_size{linkage, 512, "gpu_log_ring_buffer_size", Category::Debugging}; + Setting homebrew_nxlink_server_mode{ + linkage, HomebrewNxlinkServerMode::Disabled, "homebrew_nxlink_server_mode", + Category::Debugging}; SwitchableSetting debug_knobs{linkage, 0, diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 2db0e02a5f..5387cb7d6d 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -159,6 +159,7 @@ ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High) ENUM(TemperatureUnits, Celsius, Fahrenheit) ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3); ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All) +ENUM(HomebrewNxlinkServerMode, Disabled, EdenLog, HostStdout, File) ENUM(GameListMode, TreeView, GridView, CarouselView); ENUM(SpeedMode, Standard, Turbo, Slow); diff --git a/src/core/core.cpp b/src/core/core.cpp index 3ff42cf6f5..9f8149c541 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -52,6 +52,7 @@ #include "core/hle/service/set/system_settings_server.h" #include "core/hle/service/sm/sm.h" #include "core/internal_network/network.h" +#include "core/loader/homebrew_nxlink.h" #include "core/loader/loader.h" #include "core/memory.h" #include "core/memory/cheat_engine.h" @@ -397,6 +398,7 @@ struct System::Impl { stop_event.request_stop(); core_timing.SyncPause(false); + Loader::HomebrewNxlink::StopServer(); Network::CancelPendingSocketOperations(); kernel.SuspendEmulation(true); kernel.CloseServices(); diff --git a/src/core/loader/homebrew_nxlink.cpp b/src/core/loader/homebrew_nxlink.cpp index a216b71e09..46686b3f5a 100644 --- a/src/core/loader/homebrew_nxlink.cpp +++ b/src/core/loader/homebrew_nxlink.cpp @@ -3,11 +3,48 @@ #include "core/loader/homebrew_nxlink.h" +#include #include +#include +#include +#include +#include +#include + +#include + +#include "common/logging.h" +#include "common/polyfill_thread.h" +#include "common/thread.h" namespace Loader::HomebrewNxlink { namespace { +using boost::asio::ip::tcp; + +constexpr u16 NxlinkClientPort = 28771; +constexpr size_t ReceiveBufferSize = 1024; +constexpr size_t MaxLogLineSize = 16 * 1024; + +struct LogConnection { + explicit LogConnection(boost::asio::io_context& io_context_) : socket{io_context_} {} + + tcp::socket socket; + std::array buffer{}; + std::string line; +}; + +struct LogServerState { + LogServerState() : acceptor{io_context} {} + + boost::asio::io_context io_context; + tcp::acceptor acceptor; +}; + +std::mutex server_mutex; +std::shared_ptr server_state; +std::optional server_thread; + std::optional GetLastArgvToken(std::string_view argv_string) { while (!argv_string.empty() && argv_string.back() == '\0') { argv_string.remove_suffix(1); @@ -72,6 +109,102 @@ void AppendArgvToken(std::string& argv_string, std::string_view token) { argv_string.append(token); } +void FlushLogLine(std::string& line) { + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (!line.empty()) { + LOG_INFO(Loader, "{}", line); + line.clear(); + } +} + +void ConsumeLogBytes(LogConnection& connection, std::string_view bytes) { + for (const char byte : bytes) { + if (byte == '\n') { + FlushLogLine(connection.line); + continue; + } + + connection.line.push_back(byte); + if (connection.line.size() >= MaxLogLineSize) { + FlushLogLine(connection.line); + } + } +} + +void StartRead(std::shared_ptr connection) { + connection->socket.async_read_some( + boost::asio::buffer(connection->buffer), + [connection](const boost::system::error_code& error, size_t bytes_read) { + if (error.failed()) { + FlushLogLine(connection->line); + return; + } + + ConsumeLogBytes(*connection, {connection->buffer.data(), bytes_read}); + StartRead(connection); + }); +} + +void StartAccept(std::shared_ptr state) { + auto connection = std::make_shared(state->io_context); + state->acceptor.async_accept( + connection->socket, + [state, connection](const boost::system::error_code& error) { + if (!error.failed()) { + LOG_INFO(Loader, "Homebrew nxlink log client connected"); + StartRead(connection); + } + if (state->acceptor.is_open()) { + StartAccept(state); + } + }); +} + +void StartLogServer() { + std::scoped_lock lock{server_mutex}; + if (server_thread) { + return; + } + + auto state = std::make_shared(); + const tcp::endpoint endpoint{boost::asio::ip::address_v4::loopback(), NxlinkClientPort}; + + boost::system::error_code error; + state->acceptor.open(endpoint.protocol(), error); + if (!error.failed()) { + state->acceptor.set_option(tcp::acceptor::reuse_address(true), error); + } + if (!error.failed()) { + state->acceptor.bind(endpoint, error); + } + if (!error.failed()) { + state->acceptor.listen(boost::asio::socket_base::max_listen_connections, error); + } + if (error.failed()) { + LOG_WARNING(Loader, "Homebrew nxlink log server could not listen on 127.0.0.1:{}: {}", + NxlinkClientPort, error.message()); + return; + } + + StartAccept(state); + server_state = state; + server_thread.emplace([state](std::stop_token stop_token) { + Common::SetCurrentThreadName("HomebrewNxlink"); + std::stop_callback stop_callback{stop_token, [state] { + boost::system::error_code ignored; + state->acceptor.close(ignored); + state->io_context.stop(); + }}; + + LOG_INFO(Loader, "Homebrew nxlink log server listening on 127.0.0.1:{}", + NxlinkClientPort); + state->io_context.run(); + LOG_INFO(Loader, "Homebrew nxlink log server stopped"); + }); +} + } // namespace bool IsArgvMarker(std::string_view token) { @@ -88,6 +221,20 @@ bool IsArgvMarker(std::string_view token) { return true; } +bool IsLoopbackArgvMarker(std::string_view token) { + if (!IsArgvMarker(token)) { + return false; + } + + for (size_t i = 0; i < 8; i++) { + if (std::tolower(static_cast(token[i])) != + std::tolower(static_cast(LoopbackArgvMarker[i]))) { + return false; + } + } + return true; +} + std::optional GetArgvMarker(std::string_view argv_string) { auto last_token = GetLastArgvToken(argv_string); if (!last_token || !IsArgvMarker(*last_token)) { @@ -97,13 +244,68 @@ std::optional GetArgvMarker(std::string_view argv_string) { } std::optional PrepareArgv(std::string& argv_string, - std::string_view inherited_marker) { + std::string_view inherited_marker, + bool append_loopback_marker) { auto marker = GetArgvMarker(argv_string); if (!marker && IsArgvMarker(inherited_marker)) { AppendArgvToken(argv_string, inherited_marker); marker = std::string{inherited_marker}; } + if (!marker && append_loopback_marker) { + AppendArgvToken(argv_string, LoopbackArgvMarker); + marker = std::string{LoopbackArgvMarker}; + } return marker; } +void ApplyServerMode(Settings::HomebrewNxlinkServerMode mode, + const std::optional& active_marker) { + switch (mode) { + case Settings::HomebrewNxlinkServerMode::Disabled: + StopServer(); + return; + case Settings::HomebrewNxlinkServerMode::EdenLog: + if (active_marker && IsLoopbackArgvMarker(*active_marker)) { + StartLogServer(); + return; + } + StopServer(); + if (active_marker) { + LOG_INFO(Loader, + "Homebrew nxlink server mode overridden by argv marker '{}'; not starting " + "local nxlink log server", + *active_marker); + } else { + LOG_WARNING(Loader, + "Homebrew nxlink log server requested, but no argv marker is active"); + } + return; + case Settings::HomebrewNxlinkServerMode::HostStdout: + case Settings::HomebrewNxlinkServerMode::File: + StopServer(); + LOG_WARNING(Loader, "Homebrew nxlink server mode {} is not implemented", + static_cast(mode)); + return; + } +} + +void StopServer() { + std::shared_ptr state; + std::optional thread; + { + std::scoped_lock lock{server_mutex}; + state = std::move(server_state); + thread = std::move(server_thread); + } + + if (state) { + boost::system::error_code ignored; + state->acceptor.close(ignored); + state->io_context.stop(); + } + if (thread) { + thread->request_stop(); + } +} + } // namespace Loader::HomebrewNxlink diff --git a/src/core/loader/homebrew_nxlink.h b/src/core/loader/homebrew_nxlink.h index d5cfb70414..155c077540 100644 --- a/src/core/loader/homebrew_nxlink.h +++ b/src/core/loader/homebrew_nxlink.h @@ -8,16 +8,23 @@ #include #include +#include "common/settings_enums.h" + namespace Loader::HomebrewNxlink { constexpr size_t ArgvMarkerSize = 16; constexpr std::string_view ArgvMarkerSuffix = "_NXLINK_"; +constexpr std::string_view LoopbackArgvMarker = "0100007F_NXLINK_"; -// This module handles only the libnx argv marker. Client stream handling should be an -// explicit byte-sink API; the marker alone must not imply logging. bool IsArgvMarker(std::string_view token); +bool IsLoopbackArgvMarker(std::string_view token); std::optional GetArgvMarker(std::string_view argv_string); std::optional PrepareArgv(std::string& argv_string, - std::string_view inherited_marker); + std::string_view inherited_marker, + bool append_loopback_marker = false); + +void ApplyServerMode(Settings::HomebrewNxlinkServerMode mode, + const std::optional& active_marker); +void StopServer(); } // namespace Loader::HomebrewNxlink diff --git a/src/core/loader/nro.cpp b/src/core/loader/nro.cpp index 993662a011..c82bd37066 100644 --- a/src/core/loader/nro.cpp +++ b/src/core/loader/nro.cpp @@ -343,7 +343,8 @@ static std::optional BuildHomebrewNroImage(const std::vector BuildHomebrewNroImage(const std::vectorReadAllBytes(), nro_path, nro_file->GetName(), - launch_argv, process.GetHomebrewNxlinkArgvMarker()); + const auto nxlink_server_mode = Settings::values.homebrew_nxlink_server_mode.GetValue(); + const bool append_loopback_nxlink_marker = + nxlink_server_mode != Settings::HomebrewNxlinkServerMode::Disabled; + auto image = BuildHomebrewNroImage( + nro_file->ReadAllBytes(), nro_path, nro_file->GetName(), launch_argv, + process.GetHomebrewNxlinkArgvMarker(), append_loopback_nxlink_marker); if (!image) { LOG_WARNING(Loader, "NextLoad: in-place handoff failed because '{}' is invalid", nro_path); @@ -547,6 +553,7 @@ bool LoadNroInPlace(Core::System& system, Kernel::KProcess& process, Kernel::KTh } else { process.ClearHomebrewNxlinkArgvMarker(); } + HomebrewNxlink::ApplyServerMode(nxlink_server_mode, image->nxlink_argv_marker); system.GetFileSystemController().RegisterProcess( process.GetProcessId(), program_id, std::make_unique(loader, system.GetContentProvider(), @@ -628,7 +635,11 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process, argv_string.push_back(' '); argv_string += program_args; } - const auto nxlink_argv_marker = HomebrewNxlink::PrepareArgv(argv_string, {}); + const auto nxlink_server_mode = Settings::values.homebrew_nxlink_server_mode.GetValue(); + const bool append_loopback_nxlink_marker = + nxlink_server_mode != Settings::HomebrewNxlinkServerMode::Disabled; + const auto nxlink_argv_marker = + HomebrewNxlink::PrepareArgv(argv_string, {}, append_loopback_nxlink_marker); if (argv_string.empty() || argv_string.back() != '\0') { argv_string.push_back('\0'); } @@ -727,6 +738,7 @@ static bool LoadNroImpl(Core::System& system, Kernel::KProcess& process, } else { process.ClearHomebrewNxlinkArgvMarker(); } + HomebrewNxlink::ApplyServerMode(nxlink_server_mode, nxlink_argv_marker); { const u64 base = GetInteger(process.GetEntryPoint()); const u64 config_addr = base + args_offset_in_image;