diff --git a/CMakeLists.txt b/CMakeLists.txt
index ea354847c9..1e0db28925 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1393,6 +1393,8 @@ add_library(${CoreLibName} ${CoreLinkType}
Core/HDRemaster.cpp
Core/HDRemaster.h
Core/ThreadEventQueue.h
+ Core/WebServer.cpp
+ Core/WebServer.h
Core/Debugger/Breakpoints.cpp
Core/Debugger/Breakpoints.h
Core/Debugger/DebugInterface.h
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index 209890e9f1..b068e321e9 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -511,6 +511,7 @@
AnySuitable
+
@@ -746,6 +747,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 2efbdbd29a..fe637033f8 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -689,6 +689,9 @@
Core
+
+ Core
+
@@ -1268,6 +1271,9 @@
Core
+
+ Core
+
diff --git a/Core/WebServer.cpp b/Core/WebServer.cpp
new file mode 100644
index 0000000000..6b20646f2e
--- /dev/null
+++ b/Core/WebServer.cpp
@@ -0,0 +1,278 @@
+// 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
+#include
+#include
+#include
+#include "base/stringutil.h"
+#include "base/timeutil.h"
+#include "file/fd_util.h"
+#include "net/http_client.h"
+#include "net/http_server.h"
+#include "net/sinks.h"
+#include "thread/threadutil.h"
+#include "Common/FileUtil.h"
+#include "Common/Log.h"
+#include "Core/Config.h"
+#include "Core/WebServer.h"
+
+enum class ServerStatus {
+ STOPPED,
+ STARTING,
+ RUNNING,
+ STOPPING,
+ RESTARTING,
+};
+
+static const char *REPORT_HOSTNAME = "report.ppsspp.org";
+static const int REPORT_PORT = 80;
+
+static std::thread serverThread;
+static ServerStatus serverStatus;
+static std::mutex serverStatusLock;
+static int serverFlags;
+
+static void UpdateStatus(ServerStatus s) {
+ std::lock_guard guard(serverStatusLock);
+ serverStatus = s;
+}
+
+static bool UpdateStatus(ServerStatus s, ServerStatus old) {
+ std::lock_guard guard(serverStatusLock);
+ if (serverStatus == old) {
+ serverStatus = s;
+ return true;
+ }
+ return false;
+}
+
+static ServerStatus RetrieveStatus() {
+ std::lock_guard guard(serverStatusLock);
+ return serverStatus;
+}
+
+// This reports the local IP address to report.ppsspp.org, which can then
+// relay that address to a mobile device searching for the server.
+static void RegisterServer(int port) {
+ http::Client http;
+ Buffer theVoid;
+
+ char resource4[1024] = {};
+ if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV4)) {
+ if (http.Connect()) {
+ std::string ip = fd_util::GetLocalIP(http.sock());
+ snprintf(resource4, sizeof(resource4) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
+
+ http.GET(resource4, &theVoid);
+ theVoid.Skip(theVoid.size());
+ http.Disconnect();
+ }
+ }
+
+ if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV6)) {
+ // We register both IPv4 and IPv6 in case the other client is using a different one.
+ if (resource4[0] != 0 && http.Connect()) {
+ http.GET(resource4, &theVoid);
+ theVoid.Skip(theVoid.size());
+ http.Disconnect();
+ }
+
+ // Currently, we're not using keepalive, so gotta reconnect...
+ if (http.Connect()) {
+ char resource6[1024] = {};
+ std::string ip = fd_util::GetLocalIP(http.sock());
+ snprintf(resource6, sizeof(resource6) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
+
+ http.GET(resource6, &theVoid);
+ theVoid.Skip(theVoid.size());
+ http.Disconnect();
+ }
+ }
+}
+
+static void RegisterDiscHandlers(http::Server *http, std::unordered_map *paths) {
+ for (std::string filename : g_Config.recentIsos) {
+#ifdef _WIN32
+ static const std::string sep = "\\/";
+#else
+ static const std::string sep = "/";
+#endif
+ size_t basepos = filename.find_last_of(sep);
+ std::string basename = "/" + (basepos == filename.npos ? filename : filename.substr(basepos + 1));
+
+ // Let's not serve directories, since they won't work. Only single files.
+ // Maybe can do PBPs and other files later. Would be neat to stream virtual disc filesystems.
+ if (endsWithNoCase(basename, ".cso") || endsWithNoCase(basename, ".iso")) {
+ (*paths)[ReplaceAll(basename, " ", "%20")] = filename;
+ }
+ }
+
+ auto handler = [paths](const http::Request &request) {
+ std::string filename = (*paths)[request.resource()];
+ s64 sz = File::GetFileSize(filename);
+
+ std::string range;
+ if (request.Method() == http::RequestHeader::HEAD) {
+ request.WriteHttpResponseHeader(200, sz, "application/octet-stream", "Accept-Ranges: bytes\r\n");
+ } else if (request.GetHeader("range", &range)) {
+ s64 begin = 0, last = 0;
+ if (sscanf(range.c_str(), "bytes=%lld-%lld", &begin, &last) != 2) {
+ request.WriteHttpResponseHeader(400, -1, "text/plain");
+ request.Out()->Push("Could not understand range request.");
+ return;
+ }
+
+ if (begin < 0 || begin > last || last >= sz) {
+ request.WriteHttpResponseHeader(416, -1, "text/plain");
+ request.Out()->Push("Range goes outside of file.");
+ return;
+ }
+
+ FILE *fp = File::OpenCFile(filename, "rb");
+ if (!fp || fseek(fp, begin, SEEK_SET) != 0) {
+ request.WriteHttpResponseHeader(500, -1, "text/plain");
+ request.Out()->Push("File access failed.");
+ if (fp) {
+ fclose(fp);
+ }
+ return;
+ }
+
+ s64 len = last - begin + 1;
+ char contentRange[1024];
+ sprintf(contentRange, "Content-Range: bytes %lld-%lld/%lld\r\n", begin, last, sz);
+ request.WriteHttpResponseHeader(206, len, "application/octet-stream", contentRange);
+
+ const size_t CHUNK_SIZE = 16 * 1024;
+ char *buf = new char[CHUNK_SIZE];
+ for (s64 pos = 0; pos < len; pos += CHUNK_SIZE) {
+ s64 chunklen = std::min(len - pos, (s64)CHUNK_SIZE);
+ fread(buf, chunklen, 1, fp);
+ request.Out()->Push(buf, chunklen);
+ }
+ fclose(fp);
+ delete [] buf;
+ request.Out()->Flush();
+ } else {
+ request.WriteHttpResponseHeader(418, -1, "text/plain");
+ request.Out()->Push("This server only supports range requests.");
+ }
+ };
+
+ for (auto pair : *paths) {
+ http->RegisterHandler(pair.first.c_str(), handler);
+ }
+}
+
+static void ExecuteWebServer() {
+ setCurrentThreadName("HTTPServer");
+
+ auto http = new http::Server(new threading::NewThreadExecutor());
+ std::unordered_map discPaths;
+
+ if (serverFlags & (int)WebServerFlags::DISCS) {
+ RegisterDiscHandlers(http, &discPaths);
+ }
+
+ if (!http->Listen(g_Config.iRemoteISOPort)) {
+ if (!http->Listen(0)) {
+ ERROR_LOG(FILESYS, "Unable to listen on any port");
+ UpdateStatus(ServerStatus::STOPPED);
+ return;
+ }
+ }
+ UpdateStatus(ServerStatus::RUNNING);
+
+ g_Config.iRemoteISOPort = http->Port();
+ RegisterServer(http->Port());
+ double lastRegister = real_time_now();
+ while (RetrieveStatus() == ServerStatus::RUNNING) {
+ http->RunSlice(1.0);
+
+ double now = real_time_now();
+ if (now > lastRegister + 540.0) {
+ RegisterServer(http->Port());
+ lastRegister = now;
+ }
+ }
+
+ http->Stop();
+
+ // Move to STARTING to lock flags/STOPPING.
+ if (UpdateStatus(ServerStatus::STARTING, ServerStatus::RESTARTING)) {
+ ExecuteWebServer();
+ } else {
+ UpdateStatus(ServerStatus::STOPPED);
+ }
+}
+
+bool StartWebServer(WebServerFlags flags) {
+ std::lock_guard guard(serverStatusLock);
+ switch (serverStatus) {
+ case ServerStatus::RUNNING:
+ case ServerStatus::RESTARTING:
+ if ((serverFlags & (int)flags) == (int)flags) {
+ // Already running those flags.
+ return false;
+ }
+ serverStatus = ServerStatus::RESTARTING;
+ serverFlags |= (int)flags;
+ return true;
+
+ case ServerStatus::STOPPED:
+ serverStatus = ServerStatus::STARTING;
+ serverFlags = (int)flags;
+ serverThread = std::thread(&ExecuteWebServer);
+ serverThread.detach();
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool StopWebServer(WebServerFlags flags) {
+ std::lock_guard guard(serverStatusLock);
+ if (serverStatus != ServerStatus::RUNNING && serverStatus != ServerStatus::RESTARTING) {
+ return false;
+ }
+
+ serverFlags &= ~(int)flags;
+ if (serverFlags == 0) {
+ serverStatus = ServerStatus::STOPPING;
+ } else {
+ serverStatus = ServerStatus::RESTARTING;
+ }
+ return true;
+}
+
+bool WebServerStopping(WebServerFlags flags) {
+ std::lock_guard guard(serverStatusLock);
+ if (serverStatus == ServerStatus::RESTARTING) {
+ return (serverFlags & (int)flags) == 0;
+ }
+ return serverStatus == ServerStatus::STOPPING;
+}
+
+bool WebServerStopped(WebServerFlags flags) {
+ std::lock_guard guard(serverStatusLock);
+ if (serverStatus == ServerStatus::RUNNING) {
+ return (serverFlags & (int)flags) == 0;
+ }
+ return serverStatus == ServerStatus::STOPPED;
+}
diff --git a/Core/WebServer.h b/Core/WebServer.h
new file mode 100644
index 0000000000..db0967cbf2
--- /dev/null
+++ b/Core/WebServer.h
@@ -0,0 +1,27 @@
+// 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/.
+
+enum class WebServerFlags {
+ DISCS = 1,
+
+ ALL = 1,
+};
+
+bool StartWebServer(WebServerFlags flags);
+bool StopWebServer(WebServerFlags flags);
+bool WebServerStopping(WebServerFlags flags);
+bool WebServerStopped(WebServerFlags flags);
diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp
index 7130c7d181..8ae0bedb41 100644
--- a/UI/NativeApp.cpp
+++ b/UI/NativeApp.cpp
@@ -84,6 +84,7 @@
#include "Core/HLE/sceUsbGps.h"
#include "Core/Util/GameManager.h"
#include "Core/Util/AudioFormat.h"
+#include "Core/WebServer.h"
#include "GPU/GPUInterface.h"
#include "ui_atlas.h"
@@ -587,7 +588,8 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch
}
if (g_Config.bRemoteShareOnStartup) {
- StartRemoteISOSharing();
+ // TODO: Separate config options.
+ StartWebServer(WebServerFlags::ALL);
}
std::string sysName = System_GetProperty(SYSPROP_NAME);
diff --git a/UI/RemoteISOScreen.cpp b/UI/RemoteISOScreen.cpp
index 9831701e17..31d8f9f966 100644
--- a/UI/RemoteISOScreen.cpp
+++ b/UI/RemoteISOScreen.cpp
@@ -18,20 +18,15 @@
#include
#include
#include
-#include
#include "base/timeutil.h"
-#include "file/fd_util.h"
#include "i18n/i18n.h"
#include "json/json_reader.h"
#include "net/http_client.h"
-#include "net/http_server.h"
#include "net/resolve.h"
-#include "net/sinks.h"
-#include "thread/threadutil.h"
#include "Common/Common.h"
-#include "Common/FileUtil.h"
#include "Core/Config.h"
+#include "Core/WebServer.h"
#include "UI/RemoteISOScreen.h"
using namespace UI;
@@ -39,190 +34,9 @@ using namespace UI;
static const char *REPORT_HOSTNAME = "report.ppsspp.org";
static const int REPORT_PORT = 80;
-enum class ServerStatus {
- STOPPED,
- STARTING,
- RUNNING,
- STOPPING,
-};
-
-static std::thread *serverThread = nullptr;
-static ServerStatus serverStatus;
-static std::mutex serverStatusLock;
-static std::condition_variable serverStatusCond;
-
static bool scanCancelled = false;
static bool scanAborted = false;
-static void UpdateStatus(ServerStatus s) {
- std::lock_guard guard(serverStatusLock);
- serverStatus = s;
- serverStatusCond.notify_one();
-}
-
-static ServerStatus RetrieveStatus() {
- std::lock_guard guard(serverStatusLock);
- return serverStatus;
-}
-
-// This reports the local IP address to report.ppsspp.org, which can then
-// relay that address to a mobile device searching for the server.
-static void RegisterServer(int port) {
- http::Client http;
- Buffer theVoid;
-
- char resource4[1024] = {};
- if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV4)) {
- if (http.Connect(2, 20.0, &scanCancelled)) {
- std::string ip = fd_util::GetLocalIP(http.sock());
- snprintf(resource4, sizeof(resource4) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
-
- http.GET(resource4, &theVoid);
- theVoid.Skip(theVoid.size());
- http.Disconnect();
- }
- }
-
- if (http.Resolve(REPORT_HOSTNAME, REPORT_PORT, net::DNSType::IPV6)) {
- // We register both IPv4 and IPv6 in case the other client is using a different one.
- if (resource4[0] != 0 && http.Connect()) {
- http.GET(resource4, &theVoid);
- theVoid.Skip(theVoid.size());
- http.Disconnect();
- }
-
- // Currently, we're not using keepalive, so gotta reconnect...
- if (http.Connect()) {
- char resource6[1024] = {};
- std::string ip = fd_util::GetLocalIP(http.sock());
- snprintf(resource6, sizeof(resource6) - 1, "/match/update?local=%s&port=%d", ip.c_str(), port);
-
- http.GET(resource6, &theVoid);
- theVoid.Skip(theVoid.size());
- http.Disconnect();
- }
- }
-}
-
-static void ExecuteServer() {
- setCurrentThreadName("HTTPServer");
-
- auto http = new http::Server(new threading::SameThreadExecutor());
-
- std::map paths;
- for (std::string filename : g_Config.recentIsos) {
-#ifdef _WIN32
- static const std::string sep = "\\/";
-#else
- static const std::string sep = "/";
-#endif
- size_t basepos = filename.find_last_of(sep);
- std::string basename = "/" + (basepos == filename.npos ? filename : filename.substr(basepos + 1));
-
- // Let's not serve directories, since they won't work. Only single files.
- // Maybe can do PBPs and other files later. Would be neat to stream virtual disc filesystems.
- if (endsWithNoCase(basename, ".cso") || endsWithNoCase(basename, ".iso")) {
- paths[ReplaceAll(basename, " ", "%20")] = filename;
- }
- }
-
- auto handler = [&](const http::Request &request) {
- std::string filename = paths[request.resource()];
- s64 sz = File::GetFileSize(filename);
-
- std::string range;
- if (request.Method() == http::RequestHeader::HEAD) {
- request.WriteHttpResponseHeader(200, sz, "application/octet-stream", "Accept-Ranges: bytes\r\n");
- } else if (request.GetHeader("range", &range)) {
- s64 begin = 0, last = 0;
- if (sscanf(range.c_str(), "bytes=%lld-%lld", &begin, &last) != 2) {
- request.WriteHttpResponseHeader(400, -1, "text/plain");
- request.Out()->Push("Could not understand range request.");
- return;
- }
-
- if (begin < 0 || begin > last || last >= sz) {
- request.WriteHttpResponseHeader(416, -1, "text/plain");
- request.Out()->Push("Range goes outside of file.");
- return;
- }
-
- FILE *fp = File::OpenCFile(filename, "rb");
- if (!fp || fseek(fp, begin, SEEK_SET) != 0) {
- request.WriteHttpResponseHeader(500, -1, "text/plain");
- request.Out()->Push("File access failed.");
- if (fp) {
- fclose(fp);
- }
- return;
- }
-
- s64 len = last - begin + 1;
- char contentRange[1024];
- sprintf(contentRange, "Content-Range: bytes %lld-%lld/%lld\r\n", begin, last, sz);
- request.WriteHttpResponseHeader(206, len, "application/octet-stream", contentRange);
-
- const size_t CHUNK_SIZE = 16 * 1024;
- char *buf = new char[CHUNK_SIZE];
- for (s64 pos = 0; pos < len; pos += CHUNK_SIZE) {
- s64 chunklen = std::min(len - pos, (s64)CHUNK_SIZE);
- fread(buf, chunklen, 1, fp);
- request.Out()->Push(buf, chunklen);
- }
- fclose(fp);
- delete [] buf;
- request.Out()->Flush();
- } else {
- request.WriteHttpResponseHeader(418, -1, "text/plain");
- request.Out()->Push("This server only supports range requests.");
- }
- };
-
- for (auto pair : paths) {
- http->RegisterHandler(pair.first.c_str(), handler);
- }
-
- if (!http->Listen(g_Config.iRemoteISOPort)) {
- if (!http->Listen(0)) {
- ERROR_LOG(FILESYS, "Unable to listen on any port");
- UpdateStatus(ServerStatus::STOPPED);
- return;
- }
- }
- UpdateStatus(ServerStatus::RUNNING);
-
- g_Config.iRemoteISOPort = http->Port();
- RegisterServer(http->Port());
- double lastRegister = real_time_now();
- while (RetrieveStatus() == ServerStatus::RUNNING) {
- http->RunSlice(5.0);
-
- double now = real_time_now();
- if (now > lastRegister + 540.0) {
- RegisterServer(http->Port());
- lastRegister = now;
- }
- }
-
- http->Stop();
-
- UpdateStatus(ServerStatus::STOPPED);
-}
-
-bool StartRemoteISOSharing() {
- std::lock_guard guard(serverStatusLock);
-
- if (serverStatus != ServerStatus::STOPPED) {
- return false;
- }
-
- serverStatus = ServerStatus::STARTING;
- serverThread = new std::thread(&ExecuteServer);
- serverThread->detach();
-
- return true;
-}
-
static bool FindServer(std::string &resultHost, int &resultPort) {
http::Client http;
Buffer result;
@@ -393,11 +207,8 @@ RemoteISOScreen::RemoteISOScreen() : serverRunning_(false), serverStopping_(fals
void RemoteISOScreen::update() {
UIScreenWithBackground::update();
- bool nowRunning = RetrieveStatus() != ServerStatus::STOPPED;
+ bool nowRunning = !WebServerStopped(WebServerFlags::DISCS);
if (serverStopping_ && !nowRunning) {
- // Server stopped, delete the thread.
- delete serverThread;
- serverThread = nullptr;
serverStopping_ = false;
}
@@ -424,11 +235,10 @@ void RemoteISOScreen::CreateViews() {
rightColumnItems->SetSpacing(0.0f);
Choice *browseChoice = new Choice(ri->T("Browse Games"));
rightColumnItems->Add(browseChoice)->OnClick.Handle(this, &RemoteISOScreen::HandleBrowse);
- ServerStatus status = RetrieveStatus();
- if (status == ServerStatus::STOPPING) {
+ if (WebServerStopping(WebServerFlags::DISCS)) {
rightColumnItems->Add(new Choice(ri->T("Stopping..")))->SetDisabledPtr(&serverStopping_);
browseChoice->SetEnabled(false);
- } else if (status != ServerStatus::STOPPED) {
+ } else if (!WebServerStopped(WebServerFlags::DISCS)) {
rightColumnItems->Add(new Choice(ri->T("Stop Sharing")))->OnClick.Handle(this, &RemoteISOScreen::HandleStopServer);
browseChoice->SetEnabled(false);
} else {
@@ -450,7 +260,7 @@ void RemoteISOScreen::CreateViews() {
}
UI::EventReturn RemoteISOScreen::HandleStartServer(UI::EventParams &e) {
- if (!StartRemoteISOSharing()) {
+ if (!StartWebServer(WebServerFlags::DISCS)) {
return EVENT_SKIPPED;
}
@@ -458,13 +268,10 @@ UI::EventReturn RemoteISOScreen::HandleStartServer(UI::EventParams &e) {
}
UI::EventReturn RemoteISOScreen::HandleStopServer(UI::EventParams &e) {
- std::lock_guard guard(serverStatusLock);
-
- if (serverStatus != ServerStatus::RUNNING) {
+ if (!StopWebServer(WebServerFlags::DISCS)) {
return EVENT_SKIPPED;
}
- serverStatus = ServerStatus::STOPPING;
serverStopping_ = true;
RecreateViews();
@@ -683,13 +490,13 @@ void RemoteISOBrowseScreen::CreateViews() {
}
RemoteISOSettingsScreen::RemoteISOSettingsScreen() {
- serverRunning_ = RetrieveStatus() != ServerStatus::STOPPED;
+ serverRunning_ = !WebServerStopped(WebServerFlags::DISCS);
}
void RemoteISOSettingsScreen::update() {
UIDialogScreenWithBackground::update();
- bool nowRunning = RetrieveStatus() != ServerStatus::STOPPED;
+ bool nowRunning = !WebServerStopped(WebServerFlags::DISCS);
if (serverRunning_ != nowRunning) {
RecreateViews();
}
diff --git a/UI/RemoteISOScreen.h b/UI/RemoteISOScreen.h
index a4384d793e..b7f7ff0f5c 100644
--- a/UI/RemoteISOScreen.h
+++ b/UI/RemoteISOScreen.h
@@ -25,8 +25,6 @@
#include "UI/MiscScreens.h"
#include "UI/MainScreen.h"
-bool StartRemoteISOSharing();
-
class RemoteISOScreen : public UIScreenWithBackground {
public:
RemoteISOScreen();
diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj
index 04eba6a66f..7cd70cfa70 100644
--- a/UWP/CoreUWP/CoreUWP.vcxproj
+++ b/UWP/CoreUWP/CoreUWP.vcxproj
@@ -463,6 +463,7 @@
+
@@ -673,6 +674,7 @@
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 2c081f2ca7..428134fbcb 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -297,6 +297,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Core/Screenshot.cpp \
$(SRC)/Core/System.cpp \
$(SRC)/Core/TextureReplacer.cpp \
+ $(SRC)/Core/WebServer.cpp \
$(SRC)/Core/Debugger/Breakpoints.cpp \
$(SRC)/Core/Debugger/SymbolMap.cpp \
$(SRC)/Core/Dialog/PSPDialog.cpp \