mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 15:30:29 +02:00
The core and the headless tools no longer use mutexes, condition variables, spinlocks, std::atomic, std::thread or volatile flags. Threads hand off through libretro-common's retro_atomic and rthreads plus a few small pieces on top of them in Common/Thread: Thread, MpscQueue, EventCounter, ParkingLot waits and ReaderGate. Every wait blocks until it's woken; nothing polls on a timer. State that only one thread touches just loses its lock. The PSP filesystem, sceIo and the savedata dialog now belong to the emulation thread: async reads and writes run when they're queued and complete later in emulated time, and the IO and savedata threads are gone. The friend finder only talks to the adhoc server and hands what it gets to the emulation thread. GPU init always runs on the thread that starts the game. The Vulkan render and compile queues, pipeline and render pass caches, delete list and SPIR-V cache, the GL render queue, the software renderer's jit caches, the file loader caches, the web debugger, UPnP, input, savestates, logging and the GE debugger all hand off through queues and atomics. The IR jit's spinning profiler thread goes. A CPU thread with nothing to run now sleeps until work is queued instead of spinning. Headless with --debugger waits for a client while stopped, and ends the run once every client that connected has left. Web server shutdown keeps servicing the CPU queue until the server has finished.
259 lines
7.9 KiB
C++
259 lines
7.9 KiB
C++
// Copyright (c) 2014- 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/.
|
|
|
|
#include <cstring>
|
|
#include <string_view>
|
|
|
|
#include <retro_atomic.h>
|
|
|
|
#include "Common/Net/Cancel.h"
|
|
#include "Common/Net/HTTPServer.h"
|
|
#include "Common/Net/Sinks.h"
|
|
#include "Common/Thread/ParkingLot.h"
|
|
#include "Common/Thread/Thread.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/File/Path.h"
|
|
#include "Common/File/VFS/VFS.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/WebServer.h"
|
|
#include "Core/Debugger/WebSocket.h"
|
|
|
|
enum class ServerStatus {
|
|
STOPPED,
|
|
STARTING,
|
|
RUNNING,
|
|
STOPPING,
|
|
FINISHED,
|
|
};
|
|
|
|
// serverThread and serverStop belong to the thread calling Start/ShutdownWebServer.
|
|
static Thread serverThread;
|
|
// Set by ShutdownWebServer(); wakes the accept loop and every connection. One per server run,
|
|
// deleted only after the server thread (and with it every connection thread) is joined.
|
|
static net::CancelToken *serverStop;
|
|
static retro_atomic_int_t serverStatus{ (int)ServerStatus::STOPPED };
|
|
// See WebServerSetRequireExactPort().
|
|
static retro_atomic_int_t serverRequireExactPort{ 0 };
|
|
|
|
static void UpdateStatus(ServerStatus s) {
|
|
retro_atomic_store_release_int(&serverStatus, (int)s);
|
|
ParkingLotNotify(&serverStatus);
|
|
// ShutdownWebServer() may be waiting on the CPU thread's queue for this.
|
|
Core_WakeCPUThread();
|
|
}
|
|
|
|
static ServerStatus RetrieveStatus() {
|
|
return (ServerStatus)retro_atomic_load_acquire_int(&serverStatus);
|
|
}
|
|
|
|
static bool ServeAssetFile(const http::ServerRequest &request) {
|
|
// Skip the slash at the start of the resource path.
|
|
std::string_view filename = request.resource().substr(1);
|
|
if (filename.find("..") != std::string_view::npos) {
|
|
// Don't allow directory traversal.
|
|
return false;
|
|
}
|
|
|
|
std::string ext = Path(filename).GetFileExtension();
|
|
std::string_view mimeType = "text/plain";
|
|
if (ext == ".html") {
|
|
mimeType = "text/html";
|
|
} else if (ext == ".ico") {
|
|
mimeType = "image/x-icon";
|
|
} else if (ext == ".js") {
|
|
mimeType = "application/javascript";
|
|
} else if (ext == ".svg") {
|
|
mimeType = "image/svg+xml";
|
|
} else if (ext == ".png") {
|
|
mimeType = "image/png";
|
|
} else if (ext == ".css") {
|
|
mimeType = "text/css";
|
|
}
|
|
|
|
size_t size;
|
|
// TODO: ReadFile should take a string_view.
|
|
uint8_t *data = g_VFS.ReadFile(std::string(filename).c_str(), &size);
|
|
if (!data) {
|
|
// Try appending index.html
|
|
data = g_VFS.ReadFile((std::string(filename) + "/index.html").c_str(), &size);
|
|
mimeType = "text/html";
|
|
if (!data) {
|
|
return false;
|
|
}
|
|
INFO_LOG(Log::HTTP, "Redirected to /index.html");
|
|
}
|
|
|
|
std::string html = std::string((const char *)data, size);
|
|
delete[] data;
|
|
|
|
request.WriteHttpResponseHeader("1.0", 200, html.size(), std::string(mimeType).c_str());
|
|
request.Out()->Push(html.data(), html.size());
|
|
|
|
return true;
|
|
}
|
|
|
|
static void RedirectToDebugger(const http::ServerRequest &request) {
|
|
static const std::string payload = "Redirecting to debugger UI...\r\n";
|
|
request.WriteHttpResponseHeader("1.0", 301, payload.size(), "text/plain", "Location: /debugger/index.html\r\n");
|
|
request.Out()->Push(payload);
|
|
}
|
|
|
|
// TODO: Allow registering ServeAssetFile roots as well.
|
|
static void HandleFallback(const http::ServerRequest &request) {
|
|
SetCurrentThreadName("HandleFallback");
|
|
|
|
if (request.resource() == "/debugger/" || request.resource() == "/") {
|
|
RedirectToDebugger(request);
|
|
return;
|
|
}
|
|
if (startsWith(request.resource(), "/debugger/") && ServeAssetFile(request)) {
|
|
return;
|
|
}
|
|
|
|
static const std::string payload = "404 not found\r\n";
|
|
request.WriteHttpResponseHeader("1.0", 404, payload.size(), "text/plain");
|
|
request.Out()->Push(payload);
|
|
}
|
|
|
|
static void ForwardDebuggerRequest(const http::ServerRequest &request) {
|
|
SetCurrentThreadName("ForwardDebuggerRequest");
|
|
|
|
|
|
// Check if this is a websocket request...
|
|
std::string upgrade;
|
|
if (!request.GetHeader("upgrade", &upgrade)) {
|
|
upgrade.clear();
|
|
}
|
|
|
|
// Yes - proceed with the socket.
|
|
if (strcasecmp(upgrade.c_str(), "websocket") == 0) {
|
|
HandleDebuggerRequest(request, serverStop);
|
|
} else {
|
|
RedirectToDebugger(request);
|
|
}
|
|
}
|
|
|
|
static void WebServerThread() {
|
|
SetCurrentThreadName("HTTPServer");
|
|
|
|
auto http = new http::Server(new NewThreadExecutor());
|
|
http->SetFallbackHandler(&HandleFallback);
|
|
http->RegisterHandler("/debugger", &ForwardDebuggerRequest);
|
|
|
|
if (!http->Listen(g_Config.iRemoteISOPort, "debugger-webserver")) {
|
|
if (retro_atomic_load_acquire_int(&serverRequireExactPort)) {
|
|
// Someone asked for this specific port (--debugger=PORT) - see
|
|
// WebServerSetRequireExactPort(). Coming up on a different one would just look like
|
|
// success while nothing can connect, so give up loudly instead.
|
|
ERROR_LOG(Log::FileSystem, "Unable to listen on port %d, and it was explicitly requested (debugger - webserver). Is another PPSSPP instance already using it?", g_Config.iRemoteISOPort);
|
|
delete http;
|
|
UpdateStatus(ServerStatus::FINISHED);
|
|
return;
|
|
}
|
|
if (!http->Listen(0, "debugger-webserver")) {
|
|
ERROR_LOG(Log::FileSystem, "Unable to listen on any port (debugger - webserver)");
|
|
delete http;
|
|
UpdateStatus(ServerStatus::FINISHED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Before RUNNING, so WebServerWaitForStartup() callers can read the port.
|
|
g_Config.iRemoteISOPort = http->Port();
|
|
UpdateStatus(ServerStatus::RUNNING);
|
|
|
|
// NOTICE rather than INFO on purpose: with --debugger=0 the port is picked for us, and this
|
|
// line is the only way a client can find out which one it got.
|
|
NOTICE_LOG(Log::HTTP, "Entering web server loop. Listening on port %d", g_Config.iRemoteISOPort);
|
|
|
|
while (!serverStop->IsCancelled()) {
|
|
http->RunSlice(serverStop);
|
|
}
|
|
INFO_LOG(Log::HTTP, "Leaving web server loop.");
|
|
|
|
http->Stop();
|
|
// Joins the connection threads, which see serverStop too.
|
|
delete http;
|
|
|
|
UpdateStatus(ServerStatus::FINISHED);
|
|
INFO_LOG(Log::HTTP, "Left web server loop.");
|
|
}
|
|
|
|
bool StartWebServer() {
|
|
switch (RetrieveStatus()) {
|
|
case ServerStatus::FINISHED:
|
|
serverThread.join();
|
|
delete serverStop;
|
|
serverStop = nullptr;
|
|
[[fallthrough]];
|
|
|
|
case ServerStatus::STOPPED:
|
|
serverStop = new net::CancelToken();
|
|
UpdateStatus(ServerStatus::STARTING);
|
|
serverThread = Thread(&WebServerThread);
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void ShutdownWebServer() {
|
|
if (retro_atomic_cas_int(&serverStatus, (int)ServerStatus::RUNNING, (int)ServerStatus::STOPPING)) {
|
|
ParkingLotNotify(&serverStatus);
|
|
}
|
|
if (serverStop) {
|
|
serverStop->Cancel();
|
|
}
|
|
if (serverThread.joinable()) {
|
|
// Connections finishing up may still need the CPU thread (Core_RunOnCPUThread), and this
|
|
// is it, so keep serving them until the server thread is done.
|
|
for (;;) {
|
|
const int seen = Core_CPUWorkSeen();
|
|
Core_ProcessCPUQueue();
|
|
if (RetrieveStatus() == ServerStatus::FINISHED || RetrieveStatus() == ServerStatus::STOPPED) {
|
|
break;
|
|
}
|
|
Core_WaitForCPUWork(seen);
|
|
}
|
|
serverThread.join();
|
|
}
|
|
delete serverStop;
|
|
serverStop = nullptr;
|
|
UpdateStatus(ServerStatus::STOPPED);
|
|
}
|
|
|
|
bool WebServerRunning() {
|
|
return RetrieveStatus() == ServerStatus::RUNNING;
|
|
}
|
|
|
|
int WebServerPort() {
|
|
return g_Config.iRemoteISOPort;
|
|
}
|
|
|
|
void WebServerSetRequireExactPort(bool require) {
|
|
retro_atomic_store_release_int(&serverRequireExactPort, require ? 1 : 0);
|
|
}
|
|
|
|
bool WebServerWaitForStartup() {
|
|
ParkingLotWait(&serverStatus, [] { return RetrieveStatus() != ServerStatus::STARTING; });
|
|
return RetrieveStatus() == ServerStatus::RUNNING;
|
|
}
|