Files
ppsspp/Core/Util/PortManager.h
T
Henrik RydgårdandClaude Opus 5 2cb0be1bc4 UPnP: fix the exit hang and the CPU spin, and only run the thread when enabled
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
2026-08-30 00:37:05 +02:00

146 lines
4.8 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/
#pragma once
#ifdef USE_SYSTEM_MINIUPNPC
#include <miniupnpc/miniwget.h>
#include <miniupnpc/miniupnpc.h>
#include <miniupnpc/upnpcommands.h>
#else
#ifndef MINIUPNP_STATICLIB
#define MINIUPNP_STATICLIB
#endif
#include "ext/miniupnp/miniupnpc/include/miniwget.h"
#include "ext/miniupnp/miniupnpc/include/miniupnpc.h"
#include "ext/miniupnp/miniupnpc/include/upnpcommands.h"
#endif
#include <string>
#include <deque>
#define IP_PROTOCOL_TCP "TCP"
#define IP_PROTOCOL_UDP "UDP"
enum {
UPNP_INITSTATE_NONE = 0,
UPNP_INITSTATE_BUSY = 1,
UPNP_INITSTATE_DONE = 2,
};
enum {
UPNP_CMD_ADD = 0,
UPNP_CMD_REMOVE = 1,
};
struct UPnPArgs {
int cmd = UPNP_CMD_ADD;
std::string protocol;
unsigned short port = 0;
unsigned short intport = 0;
// Description to register the mapping under. Built when the request is queued, on the thread
// that owns the game state, since the UPnP service thread can't safely read it later.
std::string desc;
// How many times we've failed to reach the router about this request. Bounded so a request
// can't get retried forever, blocking everything queued behind it.
int attempts = 0;
};
struct PortMap {
bool taken;
std::string protocol;
std::string extPort_str;
std::string intPort_str;
std::string lanip;
std::string remoteHost;
std::string desc;
std::string duration;
std::string enabled;
};
// Only ever touched by the UPnP service thread (see PortManager.cpp). Don't call into it
// from anywhere else - queue a request with UPnP_Add()/UPnP_Remove() instead.
class PortManager {
public:
// Discover a router and pick up any mappings we left behind earlier.
// timeout: milliseconds to wait for a router to respond.
bool Initialize(unsigned int timeout = 2000);
int GetInitState() const { return m_InitState; }
// Add a port & protocol (TCP, UDP or vendor-defined) to map for forwarding (intport = 0 : same as [external] port)
bool Add(const char *protocol, unsigned short port, unsigned short intport, const std::string &desc);
// Remove a port mapping (external port)
bool Remove(const char *protocol, unsigned short port);
// Drops our mappings, restores any that we took over, and resets to the uninitialized state.
// budgetSeconds bounds how long we're willing to keep talking to the router: a router that has
// gone away answers with socket timeouts, which would otherwise stall app exit for a long time.
void Shutdown(double budgetSeconds = 3.0);
private:
// Retrieves port lists mapped by PPSSPP for current LAN IP & other's applications
bool RefreshPortList();
// Removes the port mappings we know PPSSPP created (including leftovers from previous crashes,
// which RefreshPortList() picks up at init time).
bool Clear();
// Restore ports mapped by others that were taken by PPSSPP, better used after Clear()
bool Restore();
// Uninitialize/Reset the state
void Terminate();
bool HaveControlURL() const;
// True once the current operation has used up its time budget, see Shutdown().
bool OutOfTime() const;
UPNPUrls m_urls{};
IGDdatas m_datas{};
bool m_urlsValid = false;
int m_InitState = UPNP_INITSTATE_NONE;
int m_LocalPort = UPNP_LOCAL_PORT_ANY;
double m_deadline = 0.0;
std::string m_lanip;
std::string m_leaseDuration;
std::deque<std::pair<std::string, std::string>> m_portList;
std::deque<PortMap> m_otherPortList;
};
extern PortManager g_PortManager;
void __UPnPInit(unsigned int timeout_ms);
void __UPnPShutdown();
// Add a port & protocol (TCP, UDP or vendor-defined) to map for forwarding (intport = 0 : same as [external] port)
void UPnP_Add(const char *protocol, unsigned short port, unsigned short intport = 0);
// Remove a port mapping (external port)
void UPnP_Remove(const char *protocol, unsigned short port);
// Wakes the UPnP service thread immediately, without queuing a request - call this after
// changing the enable setting so it can connect (or tear down its mappings) right away
// instead of waiting for the next retry.
void UPnP_Notify();