mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-07-11 01:24:28 +02:00
Improvements on shadnet reachabilty (#4691)
* disconnect on srv offline and protocol change * fixes for macOS
This commit is contained in:
@@ -1213,6 +1213,8 @@ add_custom_command(
|
||||
|
||||
set(SHADNET src/shadnet/client.cpp
|
||||
src/shadnet/client.h
|
||||
src/shadnet/server_probe.cpp
|
||||
src/shadnet/server_probe.h
|
||||
"${SHADNET_PROTO_OUT}/shadnet.pb.cc"
|
||||
)
|
||||
if (APPLE)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
@@ -518,6 +519,10 @@ private:
|
||||
VulkanSettings m_vulkan{};
|
||||
ConfigMode m_configMode{ConfigMode::Default};
|
||||
|
||||
// Runtime-only override: when true, IsShadNetEnabled() reports false for the
|
||||
// rest of this run regardless of the persisted setting
|
||||
std::atomic<bool> m_shadnet_session_disabled{false};
|
||||
|
||||
bool m_loaded{false};
|
||||
|
||||
static std::shared_ptr<EmulatorSettingsImpl> s_instance;
|
||||
@@ -596,7 +601,22 @@ public:
|
||||
SETTING_FORWARD_BOOL(m_general, Neo, neo_mode)
|
||||
SETTING_FORWARD_BOOL(m_general, DevKit, dev_kit_mode)
|
||||
SETTING_FORWARD(m_general, ExtraDmemInMBytes, extra_dmem_in_mbytes)
|
||||
SETTING_FORWARD_BOOL(m_general, ShadNetEnabled, shad_net_enabled)
|
||||
bool IsShadNetEnabled() const {
|
||||
return m_general.shad_net_enabled.get(m_configMode) &&
|
||||
!m_shadnet_session_disabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
void SetShadNetEnabled(bool v, bool specific = false) {
|
||||
m_general.shad_net_enabled.set(v, specific);
|
||||
}
|
||||
bool IsShadNetEnabledSetting() const {
|
||||
return m_general.shad_net_enabled.get(m_configMode);
|
||||
}
|
||||
void SetShadNetSessionDisabled(bool v) {
|
||||
m_shadnet_session_disabled.store(v, std::memory_order_relaxed);
|
||||
}
|
||||
bool IsShadNetSessionDisabled() const {
|
||||
return m_shadnet_session_disabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
SETTING_FORWARD_BOOL(m_general, TrophyPopupDisabled, trophy_popup_disabled)
|
||||
SETTING_FORWARD(m_general, TrophyNotificationDuration, trophy_notification_duration)
|
||||
SETTING_FORWARD(m_general, TrophyNotificationSide, trophy_notification_side)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "imgui/shadnet_notifications_layer.h"
|
||||
#include "np_handler.h"
|
||||
#include "shadnet.pb.h"
|
||||
#include "shadnet/server_probe.h"
|
||||
|
||||
namespace Libraries::Np {
|
||||
|
||||
@@ -102,6 +103,51 @@ void NpHandler::Initialize() {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const auto [host, port] = ParseServerAddress();
|
||||
const ShadNet::ProbeInfo probe = ShadNet::ProbeServer(host, port);
|
||||
if (probe.result != ShadNet::ProbeResult::Ok) {
|
||||
EmulatorSettings.SetShadNetSessionDisabled(true);
|
||||
switch (probe.result) {
|
||||
case ShadNet::ProbeResult::VersionMismatch:
|
||||
LOG_WARNING(NpHandler,
|
||||
"shadNet server {}:{} protocol version mismatch (server v{}, emulator "
|
||||
"v{}); disabling shadNet for this run (saved setting unchanged)",
|
||||
host, port, probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION);
|
||||
ImGui::ShadNetNotify::Push(
|
||||
ImGui::ShadNetNotify::Kind::Info,
|
||||
fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). "
|
||||
"Please update shadPS4. Online features are disabled for this "
|
||||
"session.",
|
||||
probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION));
|
||||
break;
|
||||
case ShadNet::ProbeResult::ProtocolError:
|
||||
LOG_WARNING(NpHandler,
|
||||
"shadNet server {}:{} sent an invalid ServerInfo handshake; disabling "
|
||||
"shadNet for this run (saved setting unchanged)",
|
||||
host, port);
|
||||
ImGui::ShadNetNotify::Push(
|
||||
ImGui::ShadNetNotify::Kind::Info,
|
||||
fmt::format("shadNet server ({}:{}) uses an incompatible protocol. Online "
|
||||
"features are disabled for this session.",
|
||||
host, port));
|
||||
break;
|
||||
default: // Unreachable
|
||||
LOG_WARNING(NpHandler,
|
||||
"shadNet server {}:{} is offline/unreachable; disabling shadNet for "
|
||||
"this run (saved setting unchanged)",
|
||||
host, port);
|
||||
ImGui::ShadNetNotify::Push(
|
||||
ImGui::ShadNetNotify::Kind::Info,
|
||||
fmt::format("shadNet server ({}:{}) is offline. Online features are disabled "
|
||||
"for this session.",
|
||||
host, port));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto logged_in = UserManagement.GetLoggedInUsers(); // get all login users
|
||||
int connected_count = 0;
|
||||
for (int i = 0; i < Libraries::UserService::ORBIS_USER_SERVICE_MAX_LOGIN_USERS; ++i) {
|
||||
@@ -184,10 +230,40 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
|
||||
client->SetAppearOffline(m_appear_offline.load());
|
||||
client->Start(host, port, npid, password, token);
|
||||
|
||||
// Shared handling for an incompatible-protocol failure (version mismatch or
|
||||
// corrupt/unparseable stream): tell the user and disable shadNet for this run,
|
||||
// since reconnect attempts against an incompatible server are pointless.
|
||||
const auto handle_protocol_mismatch = [&client](ShadNet::ShadNetState st) {
|
||||
if (st != ShadNet::ShadNetState::FailureProtocol)
|
||||
return;
|
||||
const u32 server_ver = client->GetServerProtocolVersion();
|
||||
EmulatorSettings.SetShadNetSessionDisabled(true);
|
||||
if (server_ver != 0) {
|
||||
LOG_ERROR(NpHandler,
|
||||
"shadNet protocol version mismatch (server v{}, emulator v{}); disabling "
|
||||
"shadNet for this run (saved setting unchanged)",
|
||||
server_ver, ShadNet::SHAD_PROTOCOL_VERSION);
|
||||
ImGui::ShadNetNotify::Push(
|
||||
ImGui::ShadNetNotify::Kind::Info,
|
||||
fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). "
|
||||
"Please update shadPS4. Online features are disabled for this "
|
||||
"session.",
|
||||
server_ver, ShadNet::SHAD_PROTOCOL_VERSION));
|
||||
} else {
|
||||
LOG_ERROR(NpHandler, "shadNet protocol error during handshake; disabling shadNet for "
|
||||
"this run (saved setting unchanged)");
|
||||
ImGui::ShadNetNotify::Push(
|
||||
ImGui::ShadNetNotify::Kind::Info,
|
||||
"shadNet server uses an incompatible protocol. Online features are disabled "
|
||||
"for this session.");
|
||||
}
|
||||
};
|
||||
|
||||
const ShadNet::ShadNetState conn_state = client->WaitForConnection();
|
||||
if (conn_state != ShadNet::ShadNetState::Ok) {
|
||||
LOG_ERROR(NpHandler, "user_id={} connection failed (state={})", user_id,
|
||||
static_cast<int>(conn_state));
|
||||
handle_protocol_mismatch(conn_state);
|
||||
client->Stop();
|
||||
return false;
|
||||
}
|
||||
@@ -196,6 +272,7 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
|
||||
if (auth_state != ShadNet::ShadNetState::Ok) {
|
||||
LOG_ERROR(NpHandler, "user_id={} authentication failed (state={})", user_id,
|
||||
static_cast<int>(auth_state));
|
||||
handle_protocol_mismatch(auth_state);
|
||||
client->Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ void SettingsWindow::LoadSettings(std::string profile) {
|
||||
directMemoryAccessSetting = EmulatorSettings.IsDirectMemoryAccessEnabled();
|
||||
devkitConsoleSetting = EmulatorSettings.IsDevKit();
|
||||
neoModeSetting = EmulatorSettings.IsNeo();
|
||||
shadnetEnabledSetting = EmulatorSettings.IsShadNetEnabled();
|
||||
shadnetEnabledSetting = EmulatorSettings.IsShadNetEnabledSetting();
|
||||
connectedNetworkSetting = EmulatorSettings.IsConnectedToNetwork();
|
||||
pipelineCacheEnabledSetting = EmulatorSettings.IsPipelineCacheEnabled();
|
||||
pipelineCacheArchiveSetting = EmulatorSettings.IsPipelineCacheArchived();
|
||||
|
||||
@@ -169,6 +169,8 @@ void ShadNetClient::ConnectThread() {
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
if (m_state == ShadNetState::FailureProtocol)
|
||||
break;
|
||||
if (m_terminate || attempt == SHAD_CONNECT_MAX_ATTEMPTS)
|
||||
break;
|
||||
LOG_WARNING(ShadNet, "ShadNet: connect attempt {}/{} to {}:{} failed, retrying in {} ms",
|
||||
@@ -400,11 +402,12 @@ bool ShadNetClient::DoConnect() {
|
||||
}
|
||||
if (payload_sz >= 4) {
|
||||
const u32 server_ver = GetLE32(si_payload.data());
|
||||
m_server_protocol_version.store(server_ver);
|
||||
if (server_ver != SHAD_PROTOCOL_VERSION) {
|
||||
LOG_ERROR(ShadNet, "Protocol version mismatch server={} client={}", server_ver,
|
||||
SHAD_PROTOCOL_VERSION);
|
||||
DoDisconnect();
|
||||
m_state = ShadNetState::FailureServerInfo;
|
||||
m_state = ShadNetState::FailureProtocol;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,9 @@ public:
|
||||
|
||||
const std::string& GetAvatarUrl() const;
|
||||
u64 GetUserId() const;
|
||||
u32 GetServerProtocolVersion() const {
|
||||
return m_server_protocol_version.load();
|
||||
}
|
||||
u32 GetAddrLocal() const;
|
||||
u32 GetAddrServer() const;
|
||||
bool IsMatching2Enabled() const;
|
||||
@@ -369,6 +372,7 @@ private:
|
||||
std::string m_bearer_token;
|
||||
std::atomic<u32> m_addr_local{0};
|
||||
std::atomic<u32> m_addr_server{0};
|
||||
std::atomic<u32> m_server_protocol_version{0};
|
||||
std::atomic<bool> m_matching2_enabled{false};
|
||||
std::atomic<bool> m_server_features_received{false};
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "common/logging/log.h"
|
||||
#include "shadnet/client.h" // socket platform typedefs + protocol constants
|
||||
#include "shadnet/server_probe.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
static void ProbePlatformInit() {
|
||||
static bool done = false;
|
||||
if (!done) {
|
||||
WSADATA wd;
|
||||
WSAStartup(MAKEWORD(2, 2), &wd);
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
static void ProbePlatformInit() {}
|
||||
#endif
|
||||
|
||||
namespace ShadNet {
|
||||
|
||||
static u32 ProbeGetLE32(const u8* p) {
|
||||
return static_cast<u32>(p[0]) | (static_cast<u32>(p[1]) << 8) | (static_cast<u32>(p[2]) << 16) |
|
||||
(static_cast<u32>(p[3]) << 24);
|
||||
}
|
||||
|
||||
// recv() exactly n bytes (blocking, bounded by the socket's SO_RCVTIMEO).
|
||||
static bool ProbeRecvN(ShadSocketHandle sock, u8* buf, u32 n) {
|
||||
u32 got = 0;
|
||||
while (got < n) {
|
||||
const int r =
|
||||
::recv(sock, reinterpret_cast<char*>(buf) + got, static_cast<int>(n - got), 0);
|
||||
if (r <= 0)
|
||||
return false;
|
||||
got += static_cast<u32>(r);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ProbeInfo ProbeServer(const std::string& host, u16 port, u32 timeout_ms) {
|
||||
ProbeInfo info{};
|
||||
|
||||
if (host.empty()) {
|
||||
LOG_WARNING(ShadNet, "probe skipped: empty shadNet host");
|
||||
return info; // Unreachable
|
||||
}
|
||||
|
||||
ProbePlatformInit();
|
||||
|
||||
struct addrinfo hints{}, *res_list = nullptr;
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
if (::getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res_list) != 0 ||
|
||||
!res_list) {
|
||||
LOG_WARNING(ShadNet, "probe: DNS resolution failed for '{}'", host);
|
||||
return info; // Unreachable
|
||||
}
|
||||
|
||||
ShadSocketHandle sock = ::socket(res_list->ai_family, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock == SHAD_INVALID_SOCK) {
|
||||
::freeaddrinfo(res_list);
|
||||
return info; // Unreachable
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
{
|
||||
u_long nb = 1;
|
||||
::ioctlsocket(sock, FIONBIO, &nb);
|
||||
}
|
||||
#else
|
||||
{
|
||||
int fl = ::fcntl(sock, F_GETFL, 0);
|
||||
::fcntl(sock, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
#endif
|
||||
|
||||
const int cr = ::connect(sock, res_list->ai_addr, static_cast<int>(res_list->ai_addrlen));
|
||||
::freeaddrinfo(res_list);
|
||||
|
||||
bool connected = false;
|
||||
#ifdef _WIN32
|
||||
const bool in_progress = (cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK);
|
||||
#else
|
||||
const bool in_progress = (cr < 0 && errno == EINPROGRESS);
|
||||
#endif
|
||||
if (cr == 0 || in_progress) {
|
||||
fd_set wfds;
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(sock, &wfds);
|
||||
struct timeval tv{};
|
||||
tv.tv_sec = static_cast<decltype(tv.tv_sec)>(timeout_ms / 1000);
|
||||
tv.tv_usec = static_cast<decltype(tv.tv_usec)>((timeout_ms % 1000) * 1000);
|
||||
if (::select(static_cast<int>(sock) + 1, nullptr, &wfds, nullptr, &tv) > 0) {
|
||||
int err = 0;
|
||||
socklen_t len = sizeof(err);
|
||||
::getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &len);
|
||||
connected = (err == 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
SHAD_CLOSE(sock);
|
||||
LOG_WARNING(ShadNet, "probe: server {}:{} is unreachable (timeout {} ms)", host, port,
|
||||
timeout_ms);
|
||||
return info; // Unreachable
|
||||
}
|
||||
|
||||
// Restore blocking mode and apply a receive timeout for the ServerInfo read.
|
||||
#ifdef _WIN32
|
||||
{
|
||||
u_long nb = 0;
|
||||
::ioctlsocket(sock, FIONBIO, &nb);
|
||||
}
|
||||
DWORD so_rcv = static_cast<DWORD>(timeout_ms);
|
||||
#else
|
||||
{
|
||||
int fl = ::fcntl(sock, F_GETFL, 0);
|
||||
::fcntl(sock, F_SETFL, fl & ~O_NONBLOCK);
|
||||
}
|
||||
struct timeval so_rcv{};
|
||||
so_rcv.tv_sec = static_cast<decltype(so_rcv.tv_sec)>(timeout_ms / 1000);
|
||||
so_rcv.tv_usec = static_cast<decltype(so_rcv.tv_usec)>((timeout_ms % 1000) * 1000);
|
||||
#endif
|
||||
::setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&so_rcv),
|
||||
sizeof(so_rcv));
|
||||
|
||||
info.result = ProbeResult::ProtocolError;
|
||||
|
||||
u8 hdr[SHAD_HEADER_SIZE];
|
||||
if (!ProbeRecvN(sock, hdr, SHAD_HEADER_SIZE)) {
|
||||
SHAD_CLOSE(sock);
|
||||
LOG_WARNING(ShadNet, "probe: {}:{} reachable but no ServerInfo header received", host,
|
||||
port);
|
||||
return info;
|
||||
}
|
||||
if (static_cast<PacketType>(hdr[0]) != PacketType::ServerInfo) {
|
||||
SHAD_CLOSE(sock);
|
||||
LOG_WARNING(ShadNet, "probe: {}:{} sent packet type {:02x} instead of ServerInfo", host,
|
||||
port, hdr[0]);
|
||||
return info;
|
||||
}
|
||||
|
||||
const u32 total_sz = ProbeGetLE32(hdr + 3);
|
||||
if (total_sz < SHAD_HEADER_SIZE || total_sz > SHAD_MAX_PACKET_SIZE) {
|
||||
SHAD_CLOSE(sock);
|
||||
LOG_WARNING(ShadNet, "probe: {}:{} sent corrupt ServerInfo (total_sz={})", host, port,
|
||||
total_sz);
|
||||
return info;
|
||||
}
|
||||
const u32 payload_sz = total_sz - SHAD_HEADER_SIZE;
|
||||
std::vector<u8> payload(payload_sz);
|
||||
if (payload_sz > 0 && !ProbeRecvN(sock, payload.data(), payload_sz)) {
|
||||
SHAD_CLOSE(sock);
|
||||
LOG_WARNING(ShadNet, "probe: {}:{} ServerInfo payload read failed", host, port);
|
||||
return info;
|
||||
}
|
||||
SHAD_CLOSE(sock);
|
||||
|
||||
if (payload_sz < 4) {
|
||||
info.result = ProbeResult::Ok;
|
||||
LOG_INFO(ShadNet, "probe: server {}:{} is reachable (no version field in ServerInfo)", host,
|
||||
port);
|
||||
return info;
|
||||
}
|
||||
|
||||
info.server_version = ProbeGetLE32(payload.data());
|
||||
if (info.server_version != SHAD_PROTOCOL_VERSION) {
|
||||
info.result = ProbeResult::VersionMismatch;
|
||||
LOG_WARNING(ShadNet, "probe: server {}:{} protocol version mismatch (server v{}, ours v{})",
|
||||
host, port, info.server_version, SHAD_PROTOCOL_VERSION);
|
||||
return info;
|
||||
}
|
||||
|
||||
info.result = ProbeResult::Ok;
|
||||
LOG_INFO(ShadNet, "probe: server {}:{} is reachable (protocol v{})", host, port,
|
||||
info.server_version);
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace ShadNet
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "common/types.h"
|
||||
|
||||
namespace ShadNet {
|
||||
|
||||
// Default timeout for the startup reachability probe.
|
||||
static constexpr u32 SHAD_PROBE_TIMEOUT_MS = 3000;
|
||||
|
||||
enum class ProbeResult {
|
||||
Ok, // reachable and protocol version matches SHAD_PROTOCOL_VERSION
|
||||
Unreachable, // DNS failure, connect failure, or timeout
|
||||
VersionMismatch, // reachable, but ServerInfo protocol version differs from ours
|
||||
ProtocolError, // reachable, but the ServerInfo handshake was malformed/absent
|
||||
};
|
||||
|
||||
struct ProbeInfo {
|
||||
ProbeResult result = ProbeResult::Unreachable;
|
||||
// Protocol version reported by the server's ServerInfo packet.
|
||||
// 0 when unknown (unreachable / malformed / server sent no version field).
|
||||
u32 server_version = 0;
|
||||
};
|
||||
|
||||
ProbeInfo ProbeServer(const std::string& host, u16 port, u32 timeout_ms = SHAD_PROBE_TIMEOUT_MS);
|
||||
|
||||
} // namespace ShadNet
|
||||
Reference in New Issue
Block a user