mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-03 19:25:18 +02:00
The two long-standing bug reports had a shared root: the service loop could end up in a state it never left. - Exit hang: UPNP_CMD_EXIT was queued alongside port requests and only acted on when it reached the front. A request that couldn't complete was never popped, so exit sat behind it forever and join() blocked indefinitely. Exit is a flag now, checked before anything else. - CPU spin: wait_for() with a predicate returns immediately when the predicate already holds, so a stuck queue head meant a tight loop. sceNetInet's bind() queues UPnP_Add regardless of the setting, so this hit whenever UPnP was off and a game used sockets. The loop always blocks now, and requests are dropped while UPnP is off. - Failed discovery was retried every 5s forever, each time a full 2s SSDP round plus an error toast. Now backs off 5s -> 300s and reports once. Other things found while in here: - Every failed Initialize() leaked a UPNPUrls + IGDdatas, so ~every 5 seconds for anyone with UPnP on and no router. The manual miniwget/parserootdesc/ GetUPNPUrls block was also redundant - UPNP_GetValidIGD does all of it and memsets over the result, leaking the URLs and costing an extra HTTP round trip per attempt. - UPNP_GetValidIGD's status was never checked, so we could go DONE with no usable IGD and hand a NULL controlURL to UPNP_GetConnectionTypeInfo. - miniupnpc's strncpy into the port-mapping-entry buffers doesn't guarantee a terminator; an 80-char description ran std::string off the end of desc[80]. - Add() marked another app's port "taken" only after our own add succeeded, so a failed add left their mapping deleted and never restored. - Clear() walked the router's entire table at exit, one HTTP round trip per index. It now deletes only what we know we mapped, and the exit cleanup has a time budget so an unreachable router can't stall shutdown. - The in-flight request stayed in the queue during the router call, so a same-port request arriving concurrently could erase it and be dropped unexecuted. - The queue is bounded, and last-write-wins per port collapses the churn from games that rebind in a loop. - The mapping description is built when the request is queued rather than read off g_paramSFO from the UPnP thread later. The thread now only exists while the setting is on - turning it off makes it remove its mappings and exit, turning it on starts one. That means __UPnPInit() has to run after the config is actually loaded; g_Config.Init() only builds a lookup table. QueueRequest() reconciles too, so a per-game config or a libretro core option enabling UPnP works without a notify at every call site. The settings checkbox is disabled in-game, since sceNet latches related settings at boot and a game that already mapped its ports wouldn't cope with them disappearing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
719 lines
26 KiB
C++
719 lines
26 KiB
C++
// Copyright (c) 2013- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
|
|
// Most of the code are based on https://github.com/RJ/libportfwd and updated to the latest miniupnp library
|
|
// All credit goes to him and the official miniupnp project! http://miniupnp.free.fr/
|
|
|
|
// Threading model: everything in PortManager runs on the UPnP service thread, which exists only
|
|
// while the UPnP setting is on. Other threads only ever push requests onto g_upnpReqs
|
|
// (UPnP_Add/UPnP_Remove) or poke the condition variable (UPnP_Notify). Talking to a router means
|
|
// blocking socket I/O with multi-second timeouts, so none of it may happen on a thread anyone
|
|
// waits for - and the thread must never end up in a state where it can't be told to stop.
|
|
|
|
#include <algorithm> // find_if
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <condition_variable>
|
|
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/System/OSD.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/System.h"
|
|
#include "Core/ELF/ParamSFO.h"
|
|
#include "Core/Util/PortManager.h"
|
|
|
|
PortManager g_PortManager;
|
|
|
|
// Guards g_upnpServiceThread itself, so that a start racing a start (or a shutdown) can't end up
|
|
// with two service threads or a std::thread being reassigned while it's still running.
|
|
static std::mutex g_upnpThreadLock;
|
|
static std::thread g_upnpServiceThread;
|
|
static bool g_upnpInitialized;
|
|
|
|
static std::mutex g_upnpLock;
|
|
static std::condition_variable g_upnpCond;
|
|
|
|
// All of the following are guarded by g_upnpLock.
|
|
static std::deque<UPnPArgs> g_upnpReqs;
|
|
static unsigned int g_upnpTimeout = 2000;
|
|
static bool g_upnpExit;
|
|
// Whether a service thread is up. The thread only exists while the setting is on, so most users
|
|
// never pay for one at all - it used to be started unconditionally and parked for the session.
|
|
static bool g_upnpThreadRunning;
|
|
// Bumped whenever there's a reason for the service thread to wake up and look around. The thread
|
|
// snapshots it before doing (slow) work, so a request that arrives while it's busy can't be missed
|
|
// and leave it asleep with something queued.
|
|
static uint32_t g_upnpWakeSeq;
|
|
// Set by UPnP_Notify() - the user flipped a setting, so retry immediately instead of continuing to
|
|
// back off from earlier failures.
|
|
static bool g_upnpResetBackoff;
|
|
|
|
// A game hammering bind() shouldn't be able to grow the queue without bound, especially while
|
|
// UPnP is unreachable and nothing is being drained.
|
|
static constexpr size_t MAX_QUEUED_REQUESTS = 64;
|
|
// After this many failed round trips we drop a request rather than let it block the queue forever.
|
|
static constexpr int MAX_REQUEST_ATTEMPTS = 3;
|
|
// Backoff bounds for rediscovering a router. Without a cap this used to redo a full SSDP discovery
|
|
// every five seconds, forever, for anyone who enabled UPnP without a router that supports it.
|
|
static constexpr double MIN_RETRY_SECONDS = 5.0;
|
|
static constexpr double MAX_RETRY_SECONDS = 300.0;
|
|
// Some routers do not support non-zero lease durations, in which case we fall back to 0 (permanent).
|
|
static const char * const DEFAULT_LEASE_DURATION = "43200"; // 12 hours, range is 0-604800 (0 = indefinite)
|
|
|
|
#ifdef WITH_UPNP
|
|
// Real routers report an error once the index runs past the end of the mapping table. This is just
|
|
// a backstop against one that never does - walking to 65536 would mean 65536 HTTP round trips.
|
|
static constexpr int MAX_PORT_MAPPINGS = 1024;
|
|
|
|
// Sizes miniupnpc copies into these buffers (see the strncpy calls in upnpcommands.c). It does not
|
|
// guarantee a terminating NUL, so we give every buffer one spare byte and zero it before each call.
|
|
struct PortMappingEntry {
|
|
char extPort[6 + 1];
|
|
char intClient[16 + 1];
|
|
char intPort[6 + 1];
|
|
char protocol[4 + 1];
|
|
char desc[80 + 1];
|
|
char enabled[4 + 1];
|
|
char rHost[64 + 1];
|
|
char duration[16 + 1];
|
|
};
|
|
#endif
|
|
|
|
static void ShowUPnPMessage(const char *key) {
|
|
auto n = GetI18NCategory(I18NCat::NETWORKING);
|
|
g_OSD.Show(OSDType::MESSAGE_INFO, n->T(key), 0.0f, "upnp_warning");
|
|
}
|
|
|
|
void PortManager::Shutdown(double budgetSeconds) {
|
|
if (m_InitState == UPNP_INITSTATE_DONE) {
|
|
m_deadline = time_now_d() + budgetSeconds;
|
|
Clear();
|
|
Restore();
|
|
m_deadline = 0.0;
|
|
}
|
|
Terminate();
|
|
}
|
|
|
|
void PortManager::Terminate() {
|
|
DEBUG_LOG(Log::Net, "PortManager::Terminate()");
|
|
#ifdef WITH_UPNP
|
|
if (m_urlsValid) {
|
|
FreeUPNPUrls(&m_urls);
|
|
m_urlsValid = false;
|
|
}
|
|
#endif
|
|
memset(&m_urls, 0, sizeof(m_urls));
|
|
memset(&m_datas, 0, sizeof(m_datas));
|
|
m_otherPortList.clear(); m_otherPortList.shrink_to_fit();
|
|
m_portList.clear(); m_portList.shrink_to_fit();
|
|
m_lanip.clear();
|
|
m_leaseDuration = DEFAULT_LEASE_DURATION;
|
|
m_LocalPort = UPNP_LOCAL_PORT_ANY;
|
|
m_deadline = 0.0;
|
|
m_InitState = UPNP_INITSTATE_NONE;
|
|
}
|
|
|
|
bool PortManager::HaveControlURL() const {
|
|
return m_urlsValid && m_urls.controlURL && m_urls.controlURL[0] != '\0';
|
|
}
|
|
|
|
bool PortManager::OutOfTime() const {
|
|
return m_deadline != 0.0 && time_now_d() > m_deadline;
|
|
}
|
|
|
|
bool PortManager::Initialize(unsigned int timeout) {
|
|
#ifdef WITH_UPNP
|
|
DEBUG_LOG(Log::Net, "PortManager::Initialize(%d)", timeout);
|
|
|
|
if (m_InitState == UPNP_INITSTATE_DONE) {
|
|
return true;
|
|
}
|
|
|
|
// A previous failed attempt can leave allocations and half-filled structs behind, so always
|
|
// start from a clean slate. This used to leak a whole UPNPUrls/IGDdatas pair per attempt.
|
|
Terminate();
|
|
m_InitState = UPNP_INITSTATE_BUSY;
|
|
|
|
int error = 0;
|
|
// m_LocalPort is UPNP_LOCAL_PORT_ANY (0), or UPNP_LOCAL_PORT_SAME (1) as an alias for 1900.
|
|
const int ipv6 = 0;
|
|
const unsigned char ttl = 2;
|
|
UPNPDev *devlist = upnpDiscover(timeout, nullptr, nullptr, m_LocalPort, ipv6, ttl, &error);
|
|
if (!devlist) {
|
|
ERROR_LOG(Log::Net, "PortManager - upnpDiscover failed (error: %i) or no UPnP device detected", error);
|
|
m_InitState = UPNP_INITSTATE_NONE;
|
|
return false;
|
|
}
|
|
|
|
for (const UPNPDev *dev = devlist; dev; dev = dev->pNext) {
|
|
INFO_LOG(Log::Net, "PortManager - UPnP device: [desc: %s] [st: %s]", dev->descURL, dev->st);
|
|
}
|
|
|
|
// UPNP_GetValidIGD downloads and parses the root descriptions itself and fills in both structs,
|
|
// so there's no need to miniwget/parserootdesc/GetUPNPUrls up front - doing that only cost an
|
|
// extra HTTP round trip to the router and leaked the URLs, since GetValidIGD memsets over them.
|
|
//
|
|
// possible "status" values:
|
|
// -1 = Internal error
|
|
// 0 = NO IGD found
|
|
// 1 = A valid connected IGD has been found
|
|
// 2 = A valid connected IGD has been found but its IP address is reserved (non routable)
|
|
// 3 = A valid IGD has been found but it reported as not connected
|
|
// 4 = an UPnP device has been found but was not recognized as an IGD
|
|
char lanaddr[64] = "";
|
|
#if (MINIUPNPC_API_VERSION >= 18)
|
|
const int status = UPNP_GetValidIGD(devlist, &m_urls, &m_datas, lanaddr, sizeof(lanaddr), nullptr, 0);
|
|
#else
|
|
const int status = UPNP_GetValidIGD(devlist, &m_urls, &m_datas, lanaddr, sizeof(lanaddr));
|
|
#endif
|
|
lanaddr[sizeof(lanaddr) - 1] = '\0';
|
|
freeUPNPDevlist(devlist);
|
|
m_urlsValid = status >= 1;
|
|
|
|
// Without a control URL there's nothing we can ask the router to do, and pressing on would just
|
|
// mean handing NULL to every UPNP_* call below.
|
|
if (!HaveControlURL()) {
|
|
ERROR_LOG(Log::Net, "PortManager - no usable IGD found (status=%d)", status);
|
|
Terminate();
|
|
return false;
|
|
}
|
|
if (status != 1) {
|
|
WARN_LOG(Log::Net, "PortManager - IGD found but in an unexpected state (status=%d), trying anyway", status);
|
|
}
|
|
|
|
m_lanip = lanaddr;
|
|
INFO_LOG(Log::Net, "PortManager - Detected LAN IP: %s (status=%d)", m_lanip.c_str(), status);
|
|
|
|
// miniupnpc writes up to 64 bytes here and doesn't guarantee a terminator, hence the spare byte.
|
|
char connectionType[64 + 1] = "";
|
|
if (UPNP_GetConnectionTypeInfo(m_urls.controlURL, m_datas.first.servicetype, connectionType) != UPNPCOMMAND_SUCCESS) {
|
|
WARN_LOG(Log::Net, "PortManager - GetConnectionTypeInfo failed");
|
|
} else {
|
|
INFO_LOG(Log::Net, "PortManager - Connection Type: %s", connectionType);
|
|
}
|
|
|
|
m_InitState = UPNP_INITSTATE_DONE;
|
|
RefreshPortList();
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif // WITH_UPNP
|
|
}
|
|
|
|
bool PortManager::Add(const char *protocol, unsigned short port, unsigned short intport, const std::string &desc) {
|
|
#ifdef WITH_UPNP
|
|
if (intport == 0)
|
|
intport = port;
|
|
INFO_LOG(Log::Net, "PortManager::Add(%s, %d, %d)", protocol, port, intport);
|
|
if (!HaveControlURL()) {
|
|
WARN_LOG(Log::Net, "PortManager::Add - the init was not done !");
|
|
Terminate();
|
|
return false;
|
|
}
|
|
|
|
char port_str[16];
|
|
char intport_str[16];
|
|
snprintf(port_str, sizeof(port_str), "%d", port);
|
|
snprintf(intport_str, sizeof(intport_str), "%d", intport);
|
|
|
|
// Nothing to do if we already mapped this port ourselves.
|
|
if (std::find_if(m_portList.begin(), m_portList.end(), [port_str, protocol](const std::pair<std::string, std::string> &el) {
|
|
return el.first == port_str && el.second == protocol;
|
|
}) != m_portList.end()) {
|
|
return true;
|
|
}
|
|
|
|
auto other = std::find_if(m_otherPortList.begin(), m_otherPortList.end(), [port_str, protocol](const PortMap &el) {
|
|
return el.extPort_str == port_str && el.protocol == protocol;
|
|
});
|
|
if (other != m_otherPortList.end()) {
|
|
// Someone else holds this port - drop their mapping first. Mark it as taken as soon as the
|
|
// delete goes through, not once our own mapping is in place: if the add below fails we still
|
|
// owe them a Restore(). This also covers a dangling mapping of ours from a session that
|
|
// didn't shut down cleanly.
|
|
if (UPNP_DeletePortMapping(m_urls.controlURL, m_datas.first.servicetype, port_str, protocol, nullptr) == 0)
|
|
other->taken = true;
|
|
}
|
|
|
|
int r = UPNP_AddPortMapping(m_urls.controlURL, m_datas.first.servicetype,
|
|
port_str, intport_str, m_lanip.c_str(), desc.c_str(), protocol, nullptr, m_leaseDuration.c_str());
|
|
if (r == 725 && m_leaseDuration != "0") {
|
|
// OnlyPermanentLeasesSupported - remember that for the rest of the session.
|
|
m_leaseDuration = "0";
|
|
r = UPNP_AddPortMapping(m_urls.controlURL, m_datas.first.servicetype,
|
|
port_str, intport_str, m_lanip.c_str(), desc.c_str(), protocol, nullptr, m_leaseDuration.c_str());
|
|
}
|
|
if (r != 0) {
|
|
ERROR_LOG(Log::Net, "PortManager - AddPortMapping failed (error: %i)", r);
|
|
if (r == UPNPCOMMAND_HTTP_ERROR) {
|
|
// Usually means the router is no longer reachable (changed networks, went to sleep).
|
|
// Invalidate the state so we rediscover instead of eating a timeout on every request.
|
|
ShowUPnPMessage("UPnP need to be reinitialized");
|
|
Terminate();
|
|
return false;
|
|
}
|
|
// Some other UPnP-level refusal. Report it as handled - retrying won't help.
|
|
return true;
|
|
}
|
|
|
|
m_portList.push_front({ port_str, protocol });
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif // WITH_UPNP
|
|
}
|
|
|
|
bool PortManager::Remove(const char *protocol, unsigned short port) {
|
|
#ifdef WITH_UPNP
|
|
INFO_LOG(Log::Net, "PortManager::Remove(%s, %d)", protocol, port);
|
|
if (!HaveControlURL()) {
|
|
WARN_LOG(Log::Net, "PortManager::Remove - the init was not done !");
|
|
Terminate();
|
|
return false;
|
|
}
|
|
|
|
char port_str[16];
|
|
snprintf(port_str, sizeof(port_str), "%d", port);
|
|
int r = UPNP_DeletePortMapping(m_urls.controlURL, m_datas.first.servicetype, port_str, protocol, nullptr);
|
|
if (r != 0) {
|
|
ERROR_LOG(Log::Net, "PortManager - DeletePortMapping failed (error: %i)", r);
|
|
if (r == UPNPCOMMAND_HTTP_ERROR) {
|
|
ShowUPnPMessage("UPnP need to be reinitialized");
|
|
Terminate();
|
|
return false;
|
|
}
|
|
}
|
|
for (auto it = m_portList.begin(); it != m_portList.end(); ) {
|
|
(it->first == port_str && it->second == protocol) ? it = m_portList.erase(it) : ++it;
|
|
}
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif // WITH_UPNP
|
|
}
|
|
|
|
bool PortManager::Restore() {
|
|
#ifdef WITH_UPNP
|
|
VERBOSE_LOG(Log::Net, "PortManager::Restore()");
|
|
if (!HaveControlURL()) {
|
|
WARN_LOG(Log::Net, "PortManager::Restore - the init was not done !");
|
|
return false;
|
|
}
|
|
for (PortMap &entry : m_otherPortList) {
|
|
if (!entry.taken)
|
|
continue;
|
|
if (OutOfTime()) {
|
|
WARN_LOG(Log::Net, "PortManager::Restore - out of time, leaving the rest to the router's lease timeout");
|
|
return false;
|
|
}
|
|
// Remove it first if it's still being held by PPSSPP.
|
|
auto el_it = std::find_if(m_portList.begin(), m_portList.end(), [&entry](const std::pair<std::string, std::string> &el) {
|
|
return el.first == entry.extPort_str && el.second == entry.protocol;
|
|
});
|
|
if (el_it != m_portList.end()) {
|
|
int r = UPNP_DeletePortMapping(m_urls.controlURL, m_datas.first.servicetype, entry.extPort_str.c_str(), entry.protocol.c_str(), nullptr);
|
|
if (r == 0) {
|
|
m_portList.erase(el_it);
|
|
} else {
|
|
ERROR_LOG(Log::Net, "PortManager::Restore - DeletePortMapping failed (error: %i)", r);
|
|
if (r == UPNPCOMMAND_HTTP_ERROR)
|
|
return false; // The router is gone, the rest of the loop would just be timeouts.
|
|
}
|
|
}
|
|
// Add the original owner back.
|
|
int r = UPNP_AddPortMapping(m_urls.controlURL, m_datas.first.servicetype,
|
|
entry.extPort_str.c_str(), entry.intPort_str.c_str(), entry.lanip.c_str(), entry.desc.c_str(),
|
|
entry.protocol.c_str(), entry.remoteHost.c_str(), entry.duration.c_str());
|
|
if (r == 0) {
|
|
entry.taken = false;
|
|
} else {
|
|
ERROR_LOG(Log::Net, "PortManager::Restore - AddPortMapping failed (error: %i)", r);
|
|
if (r == UPNPCOMMAND_HTTP_ERROR)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif // WITH_UPNP
|
|
}
|
|
|
|
bool PortManager::Clear() {
|
|
#ifdef WITH_UPNP
|
|
VERBOSE_LOG(Log::Net, "PortManager::Clear()");
|
|
if (!HaveControlURL()) {
|
|
WARN_LOG(Log::Net, "PortManager::Clear - the init was not done !");
|
|
return false;
|
|
}
|
|
// m_portList holds every mapping we know PPSSPP owns: RefreshPortList() seeds it at init time
|
|
// (so leftovers from a session that crashed are in there), and Add/Remove keep it current. That
|
|
// beats re-walking the router's whole table here, which cost an HTTP round trip per entry at
|
|
// the worst possible moment - app exit.
|
|
while (!m_portList.empty()) {
|
|
if (OutOfTime()) {
|
|
WARN_LOG(Log::Net, "PortManager::Clear - out of time, %d mapping(s) left for the router's lease timeout", (int)m_portList.size());
|
|
return false;
|
|
}
|
|
const std::pair<std::string, std::string> &entry = m_portList.front();
|
|
int r = UPNP_DeletePortMapping(m_urls.controlURL, m_datas.first.servicetype, entry.first.c_str(), entry.second.c_str(), nullptr);
|
|
if (r != 0) {
|
|
ERROR_LOG(Log::Net, "PortManager::Clear - DeletePortMapping(%s, %s) failed (error: %i)", entry.first.c_str(), entry.second.c_str(), r);
|
|
if (r == UPNPCOMMAND_HTTP_ERROR)
|
|
return false;
|
|
}
|
|
m_portList.pop_front();
|
|
}
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif // WITH_UPNP
|
|
}
|
|
|
|
bool PortManager::RefreshPortList() {
|
|
#ifdef WITH_UPNP
|
|
INFO_LOG(Log::Net, "PortManager::RefreshPortList()");
|
|
if (!HaveControlURL()) {
|
|
WARN_LOG(Log::Net, "PortManager::RefreshPortList - the init was not done !");
|
|
return false;
|
|
}
|
|
m_portList.clear();
|
|
m_otherPortList.clear();
|
|
|
|
PortMappingEntry e;
|
|
int i = 0;
|
|
int r;
|
|
do {
|
|
if (OutOfTime())
|
|
return false;
|
|
char index[16];
|
|
snprintf(index, sizeof(index), "%d", i);
|
|
memset(&e, 0, sizeof(e));
|
|
r = UPNP_GetGenericPortMappingEntry(m_urls.controlURL, m_datas.first.servicetype, index,
|
|
e.extPort, e.intClient, e.intPort, e.protocol, e.desc, e.enabled, e.rHost, e.duration);
|
|
if (r == 0) {
|
|
std::string desc = e.desc;
|
|
// Some routers prefix the description with "UPnP:", which we need to strip so it doesn't
|
|
// accumulate another prefix each time we restore the mapping.
|
|
if (startsWith(desc, "UPnP:"))
|
|
desc = desc.substr(5);
|
|
if (e.intClient == m_lanip && desc.find("PPSSPP:") != std::string::npos) {
|
|
// Ours, possibly left over from an earlier session. Clear() will drop it.
|
|
m_portList.push_back({ e.extPort, e.protocol });
|
|
} else {
|
|
// Someone else's, which we may end up taking over (and then have to put back).
|
|
m_otherPortList.push_back({ false, e.protocol, e.extPort, e.intPort, e.intClient, e.rHost, desc, e.duration, e.enabled });
|
|
}
|
|
}
|
|
i++;
|
|
} while (r == 0 && i < MAX_PORT_MAPPINGS);
|
|
|
|
if (i >= MAX_PORT_MAPPINGS)
|
|
WARN_LOG(Log::Net, "PortManager::RefreshPortList - stopped after %d entries", MAX_PORT_MAPPINGS);
|
|
INFO_LOG(Log::Net, "PortManager - %d existing PPSSPP mapping(s), %d belonging to others",
|
|
(int)m_portList.size(), (int)m_otherPortList.size());
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif // WITH_UPNP
|
|
}
|
|
|
|
// --- Service thread ---
|
|
|
|
static void DiscardQueuedRequests() {
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
if (!g_upnpReqs.empty()) {
|
|
DEBUG_LOG(Log::Net, "UPnPService: discarding %d queued request(s)", (int)g_upnpReqs.size());
|
|
g_upnpReqs.clear();
|
|
}
|
|
}
|
|
|
|
// Works through the queue until it's empty or the router stops answering.
|
|
// Returns false if we lost the router, in which case the unfinished request stays queued.
|
|
static bool ProcessQueuedRequests() {
|
|
while (true) {
|
|
UPnPArgs arg;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
if (g_upnpExit || g_upnpReqs.empty())
|
|
return true;
|
|
// Take it out of the queue for the duration. Talking to the router happens without the
|
|
// lock held, and QueueRequest() supersedes pending requests for the same port - if the
|
|
// in-flight one were still in the deque it could be erased out from under us, and we'd
|
|
// then drop whatever replaced it without ever running it.
|
|
arg = std::move(g_upnpReqs.front());
|
|
g_upnpReqs.pop_front();
|
|
}
|
|
|
|
bool ok;
|
|
switch (arg.cmd) {
|
|
case UPNP_CMD_ADD:
|
|
ok = g_PortManager.Add(arg.protocol.c_str(), arg.port, arg.intport, arg.desc);
|
|
break;
|
|
case UPNP_CMD_REMOVE:
|
|
ok = g_PortManager.Remove(arg.protocol.c_str(), arg.port);
|
|
break;
|
|
default:
|
|
ok = true;
|
|
break;
|
|
}
|
|
if (ok)
|
|
continue;
|
|
|
|
// The router stopped answering, and Add()/Remove() have already reset us to disconnected.
|
|
// Put the request back for after we reconnect and stop draining - everything behind it
|
|
// would just fail the same way.
|
|
if (++arg.attempts < MAX_REQUEST_ATTEMPTS) {
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
const bool superseded = std::any_of(g_upnpReqs.begin(), g_upnpReqs.end(), [&arg](const UPnPArgs &req) {
|
|
return req.port == arg.port && req.protocol == arg.protocol;
|
|
});
|
|
if (!superseded && g_upnpReqs.size() < MAX_QUEUED_REQUESTS)
|
|
g_upnpReqs.push_front(std::move(arg));
|
|
} else {
|
|
WARN_LOG(Log::Net, "UPnPService: giving up on %s port %d after %d attempts",
|
|
arg.protocol.c_str(), arg.port, MAX_REQUEST_ATTEMPTS);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static int upnpService(unsigned int timeout) {
|
|
SetCurrentThreadName("UPnPService");
|
|
INFO_LOG(Log::Net, "UPnPService: Begin of UPnPService Thread");
|
|
|
|
int failCount = 0;
|
|
bool wasEnabled = false;
|
|
// Absolute time before which we won't try to (re)discover a router. This has to be a deadline
|
|
// rather than a sleep length: an incoming request wakes us early, and without it every single
|
|
// request would trigger another full SSDP discovery while the router is unreachable.
|
|
double nextInitTime = 0.0;
|
|
|
|
while (true) {
|
|
uint32_t seq;
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
if (g_upnpExit)
|
|
break;
|
|
seq = g_upnpWakeSeq;
|
|
if (g_upnpResetBackoff) {
|
|
// The user just flipped the setting - don't make them wait out an old backoff.
|
|
g_upnpResetBackoff = false;
|
|
failCount = 0;
|
|
nextInitTime = 0.0;
|
|
}
|
|
}
|
|
|
|
// 0 means "sleep until someone wakes us" - which is what this thread does for the whole
|
|
// session for most users, rather than waking up periodically to find nothing to do.
|
|
double wakeAt = 0.0;
|
|
|
|
if (!g_Config.bEnableUPnP) {
|
|
// Callers queue requests without checking the setting (see bind() in sceNetInet), so throw
|
|
// them away here. Otherwise the queue grows without bound and, worse, the wait below never
|
|
// blocks - which is what pegged a core whenever UPnP was off but a game was using sockets.
|
|
DiscardQueuedRequests();
|
|
if (wasEnabled) {
|
|
// Turned off at runtime - take our mappings back down right away.
|
|
INFO_LOG(Log::Net, "UPnPService: UPnP was disabled, cleaning up");
|
|
g_PortManager.Shutdown();
|
|
wasEnabled = false;
|
|
}
|
|
failCount = 0;
|
|
nextInitTime = 0.0;
|
|
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
if (!g_Config.bEnableUPnP) {
|
|
// Nothing left to do until the setting comes back on, and StartUPnPService() will
|
|
// spin up a fresh thread for that. Deciding this under the lock is what makes that
|
|
// safe: a start that observed us still running can't be left without a thread.
|
|
g_upnpThreadRunning = false;
|
|
break;
|
|
}
|
|
// Turned back on while we were cleaning up (which involves network round trips, so
|
|
// there's real time in there) - carry on rather than exiting.
|
|
continue;
|
|
} else {
|
|
wasEnabled = true;
|
|
if (g_PortManager.GetInitState() == UPNP_INITSTATE_NONE && time_now_d() >= nextInitTime) {
|
|
if (g_PortManager.Initialize(timeout)) {
|
|
failCount = 0;
|
|
nextInitTime = 0.0;
|
|
} else {
|
|
// Only complain the first time, not once per retry.
|
|
if (failCount == 0)
|
|
ShowUPnPMessage("Unable to find UPnP device");
|
|
failCount++;
|
|
const double backoff = std::min(MIN_RETRY_SECONDS * (1 << std::min(failCount - 1, 8)), MAX_RETRY_SECONDS);
|
|
nextInitTime = time_now_d() + backoff;
|
|
}
|
|
}
|
|
|
|
if (g_PortManager.GetInitState() == UPNP_INITSTATE_DONE) {
|
|
if (!ProcessQueuedRequests()) {
|
|
// Lost the router mid-request. Whatever's left stays queued for after we reconnect.
|
|
nextInitTime = time_now_d() + MIN_RETRY_SECONDS;
|
|
}
|
|
}
|
|
if (g_PortManager.GetInitState() != UPNP_INITSTATE_DONE)
|
|
wakeAt = nextInitTime;
|
|
}
|
|
|
|
std::unique_lock<std::mutex> lock(g_upnpLock);
|
|
auto shouldWake = [seq] { return g_upnpExit || g_upnpWakeSeq != seq; };
|
|
if (wakeAt == 0.0) {
|
|
g_upnpCond.wait(lock, shouldWake);
|
|
} else {
|
|
const double delay = std::max(wakeAt - time_now_d(), 0.0);
|
|
g_upnpCond.wait_for(lock, std::chrono::duration<double>(delay), shouldWake);
|
|
}
|
|
}
|
|
|
|
// Clean up regardless of g_Config.bEnableUPnP, to avoid leaving open ports on the router.
|
|
g_PortManager.Shutdown();
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
g_upnpReqs.clear();
|
|
}
|
|
|
|
INFO_LOG(Log::Net, "UPnPService: End of UPnPService Thread");
|
|
return 0;
|
|
}
|
|
|
|
// Starts the service thread if the setting is on and it isn't up already.
|
|
static void StartUPnPService() {
|
|
std::lock_guard<std::mutex> threadLock(g_upnpThreadLock);
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
if (!g_upnpInitialized || g_upnpExit || !g_Config.bEnableUPnP)
|
|
return;
|
|
if (g_upnpThreadRunning) {
|
|
// Already up - it'll notice whatever changed on its own.
|
|
return;
|
|
}
|
|
g_upnpThreadRunning = true;
|
|
g_upnpResetBackoff = true;
|
|
}
|
|
// A previous thread may have exited when the setting was turned off. By the time it clears
|
|
// g_upnpThreadRunning it has already cleaned up after itself, so this doesn't block on a router.
|
|
if (g_upnpServiceThread.joinable())
|
|
g_upnpServiceThread.join();
|
|
g_upnpServiceThread = std::thread(upnpService, g_upnpTimeout);
|
|
}
|
|
|
|
void __UPnPInit(unsigned int timeout) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
g_upnpExit = false;
|
|
g_upnpReqs.clear();
|
|
g_upnpInitialized = true;
|
|
g_upnpTimeout = timeout;
|
|
}
|
|
// Only actually spawns a thread if UPnP is enabled; otherwise UPnP_Notify() starts one when
|
|
// the user turns it on.
|
|
StartUPnPService();
|
|
}
|
|
|
|
void __UPnPShutdown() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
g_upnpInitialized = false;
|
|
g_upnpExit = true;
|
|
// Anything still queued is moot, and dropping it here means the thread won't try to talk to
|
|
// a router we may no longer be able to reach on its way out.
|
|
g_upnpReqs.clear();
|
|
g_upnpWakeSeq++;
|
|
}
|
|
g_upnpCond.notify_all();
|
|
|
|
std::lock_guard<std::mutex> threadLock(g_upnpThreadLock);
|
|
if (g_upnpServiceThread.joinable()) {
|
|
INFO_LOG(Log::Net, "Waiting for upnp thread to shut down...");
|
|
g_upnpServiceThread.join();
|
|
INFO_LOG(Log::Net, "upnp thread shut down.");
|
|
}
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
g_upnpThreadRunning = false;
|
|
}
|
|
|
|
static void QueueRequest(UPnPArgs args) {
|
|
// The enable setting can change after startup without the settings UI being involved - a
|
|
// per-game config, or a libretro core option. Reconcile here rather than requiring every place
|
|
// that can flip it to remember to call UPnP_Notify(): this starts a thread if the setting is on
|
|
// and there isn't one, and is a cheap no-op otherwise. The other direction takes care of itself,
|
|
// since queuing below wakes a thread that then notices the setting is off and cleans up.
|
|
StartUPnPService();
|
|
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
// With UPnP off there's no service thread to drain the queue, so don't let one build up.
|
|
if (!g_upnpThreadRunning || g_upnpExit)
|
|
return;
|
|
|
|
// Last request wins for a given port: this collapses the repeated adds that games produce when
|
|
// they rebind in a loop, and lets a remove cancel an add that hasn't been sent yet.
|
|
for (auto it = g_upnpReqs.begin(); it != g_upnpReqs.end(); ) {
|
|
(it->port == args.port && it->protocol == args.protocol) ? it = g_upnpReqs.erase(it) : ++it;
|
|
}
|
|
if (g_upnpReqs.size() >= MAX_QUEUED_REQUESTS) {
|
|
WARN_LOG(Log::Net, "UPnP request queue is full, dropping request for %s port %d", args.protocol.c_str(), args.port);
|
|
return;
|
|
}
|
|
|
|
g_upnpReqs.push_back(std::move(args));
|
|
g_upnpWakeSeq++;
|
|
g_upnpCond.notify_one();
|
|
}
|
|
|
|
// Built here rather than on the service thread, which can't safely read the game state.
|
|
// Some routers automatically prefix the description with "UPnP:".
|
|
static std::string MappingDescription() {
|
|
if (PSP_IsInited()) {
|
|
return "PPSSPP:" + g_paramSFO.GetDiscID() + ":" + g_Config.sNickName;
|
|
}
|
|
return "PPSSPP:at_menu:" + g_Config.sNickName;
|
|
}
|
|
|
|
void UPnP_Add(const char *protocol, unsigned short port, unsigned short intport) {
|
|
QueueRequest({ UPNP_CMD_ADD, protocol, port, intport ? intport : port, MappingDescription() });
|
|
}
|
|
|
|
void UPnP_Remove(const char *protocol, unsigned short port) {
|
|
QueueRequest({ UPNP_CMD_REMOVE, protocol, port, port });
|
|
}
|
|
|
|
void UPnP_Notify() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(g_upnpLock);
|
|
g_upnpWakeSeq++;
|
|
g_upnpResetBackoff = true;
|
|
}
|
|
g_upnpCond.notify_one();
|
|
// Turned on: start a thread. Turned off: the running one cleans up and exits by itself, which
|
|
// we deliberately don't wait for here - this is called from the UI thread, and the cleanup
|
|
// means talking to a router that may be slow to answer.
|
|
StartUPnPService();
|
|
}
|