ES-DE is a frontend for browsing and launching games from your
@@ -38,6 +38,12 @@
+
+ https://gitlab.com/es-de/emulationstation-de/-/releases
+
+
+ https://gitlab.com/es-de/emulationstation-de/-/releases
+
https://gitlab.com/es-de/emulationstation-de/-/releases
diff --git a/es-app/src/CollectionSystemsManager.cpp b/es-app/src/CollectionSystemsManager.cpp
index 300cd600c..4ac172ba0 100644
--- a/es-app/src/CollectionSystemsManager.cpp
+++ b/es-app/src/CollectionSystemsManager.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// CollectionSystemsManager.cpp
//
// Manages collections of the following two types:
@@ -160,6 +160,8 @@ void CollectionSystemsManager::saveCustomCollection(SystemData* sys)
configFileIn.open(getCustomCollectionConfigPath(name));
#endif
for (std::string gameEntry; getline(configFileIn, gameEntry);) {
+ // Remove Windows carriage return characters.
+ gameEntry = Utils::String::replace(gameEntry, "\r", "");
std::string gamePath {Utils::String::replace(gameEntry, "%ROMPATH%", rompath)};
gamePath = Utils::String::replace(gamePath, "//", "/");
// Only add the entry if it doesn't exist, i.e. only add missing files.
@@ -1064,6 +1066,8 @@ void CollectionSystemsManager::reactivateCustomCollectionEntry(FileData* game)
std::ifstream input {path};
#endif
for (std::string gameKey; getline(input, gameKey);) {
+ // Remove Windows carriage return characters.
+ gameKey = Utils::String::replace(gameKey, "\r", "");
if (gameKey == gamePath) {
setEditMode(it->first, false);
toggleGameInCollection(game);
@@ -1331,7 +1335,8 @@ void CollectionSystemsManager::populateCustomCollection(CollectionSystemData* sy
// it's possible to use either absolute ROM paths in the collection files or using
// the path variable. The absolute ROM paths are only used for backward compatibility
// with old custom collections. All custom collections saved by ES-DE will use the
- // %ROMPATH% variable instead.
+ // %ROMPATH% variable instead. Also remove Windows carriage return characters.
+ gameKey = Utils::String::replace(gameKey, "\r", "");
gameKey = Utils::String::replace(gameKey, "%ROMPATH%", rompath);
gameKey = Utils::String::replace(gameKey, "//", "/");
diff --git a/es-app/src/CollectionSystemsManager.h b/es-app/src/CollectionSystemsManager.h
index 35162271e..a1b07b02b 100644
--- a/es-app/src/CollectionSystemsManager.h
+++ b/es-app/src/CollectionSystemsManager.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// CollectionSystemsManager.h
//
// Manages collections of the following two types:
diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp
index 9b0487993..1f143c8ac 100644
--- a/es-app/src/FileData.cpp
+++ b/es-app/src/FileData.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// FileData.cpp
//
// Provides game file data structures and functions to access and sort this information.
@@ -10,6 +10,7 @@
#include "FileData.h"
+#include "AudioManager.h"
#include "CollectionSystemsManager.h"
#include "FileFilterIndex.h"
#include "FileSorts.h"
@@ -968,6 +969,7 @@ void FileData::launchGame()
size_t coreFilePos {0};
bool foundCoreFile {false};
std::vector emulatorCorePaths;
+ bool isAndroidApp {false};
#if defined(__ANDROID__)
std::string androidPackage;
@@ -1085,8 +1087,79 @@ void FileData::launchGame()
}
// Check that the emulator actually exists, and if so, get its path.
- const std::pair emulator {
- findEmulator(command, false)};
+ std::pair emulator;
+
+#if defined(__ANDROID__)
+ // Native Android apps and games.
+ if (command.find("%ANDROIDAPP%=") != std::string::npos) {
+ std::string packageName;
+ size_t startPos {command.find("%ANDROIDAPP%=")};
+ size_t endPos {command.find(" ", startPos)};
+ if (endPos == std::string::npos)
+ endPos = command.length();
+
+ packageName = command.substr(startPos + 13, endPos - startPos - 13);
+ isAndroidApp = true;
+
+ if (packageName == "%FILEINJECT%") {
+ LOG(LogDebug) << "Injecting app info from file \"" + fileName + "\"";
+ std::string appString;
+ std::ifstream injectFileStream;
+
+ injectFileStream.open(romRaw);
+ for (std::string line; getline(injectFileStream, line);) {
+ // Remove Windows carriage return characters.
+ line = Utils::String::replace(line, "\r", "");
+ appString += line;
+ if (appString.size() > 4096)
+ break;
+ }
+ injectFileStream.close();
+
+ if (appString.empty()) {
+ LOG(LogDebug) << "FileData::launchGame(): File empty or insufficient permissions, "
+ "nothing to inject";
+ packageName = "";
+ }
+ else if (appString.size() > 4096) {
+ LOG(LogWarning) << "FileData::launchGame(): Injection file exceeding maximum "
+ "allowed size of 4096 bytes, skipping \""
+ << fileName << "\"";
+ packageName = "";
+ }
+ else {
+ packageName = appString;
+ }
+ }
+
+ if (packageName != "" && packageName != "%FILEINJECT%") {
+ LOG(LogInfo) << "Game entry is an Android app: " << packageName;
+
+ size_t separatorPos {packageName.find('/')};
+
+ if (separatorPos != std::string::npos) {
+ androidActivity = packageName.substr(separatorPos + 1);
+ packageName = packageName.substr(0, separatorPos);
+ }
+
+ if (Utils::Platform::Android::checkEmulatorInstalled(packageName, androidActivity)) {
+ emulator = std::make_pair(packageName,
+ FileData::findEmulatorResult::FOUND_ANDROID_PACKAGE);
+ }
+ else {
+ emulator = std::make_pair(packageName, FileData::findEmulatorResult::NOT_FOUND);
+ }
+ }
+ else {
+ emulator = std::make_pair(packageName, FileData::findEmulatorResult::NOT_FOUND);
+ }
+ }
+ else {
+ emulator = findEmulator(command, false);
+ }
+#else
+ emulator = findEmulator(command, false);
+#endif
// Show an error message if there was no matching emulator entry in es_find_rules.xml.
if (emulator.second == FileData::findEmulatorResult::NO_RULES) {
@@ -1102,7 +1175,12 @@ void FileData::launchGame()
return;
}
else if (emulator.second == FileData::findEmulatorResult::NOT_FOUND) {
- LOG(LogError) << "Couldn't launch game, emulator not found";
+ if (isAndroidApp) {
+ LOG(LogError) << "Couldn't launch app as it does not seem to be installed";
+ }
+ else {
+ LOG(LogError) << "Couldn't launch game, emulator not found";
+ }
LOG(LogError) << "Raw emulator launch command:";
LOG(LogError) << commandRaw;
@@ -1115,14 +1193,37 @@ void FileData::launchGame()
if (endPos != std::string::npos)
emulatorName = command.substr(startPos + 10, endPos - startPos - 10);
}
+#if defined(__ANDROID__)
+ else if ((startPos = command.find("%ANDROIDAPP%=")) != std::string::npos) {
+ endPos = command.find(" ", startPos);
+ if (endPos == std::string::npos)
+ endPos = command.length();
- if (emulatorName == "")
- window->queueInfoPopup("ERROR: COULDN'T FIND EMULATOR, HAS IT BEEN PROPERLY INSTALLED?",
- 6000);
- else
- window->queueInfoPopup("ERROR: COULDN'T FIND EMULATOR '" + emulatorName +
- "', HAS IT BEEN PROPERLY INSTALLED?",
- 6000);
+ emulatorName = command.substr(startPos + 13, endPos - startPos - 13);
+ }
+#endif
+ if (isAndroidApp) {
+ if (emulatorName == "" || emulatorName == "%FILEINJECT%") {
+ window->queueInfoPopup("ERROR: COULDN'T FIND APP, HAS IT BEEN PROPERLY INSTALLED?",
+ 6000);
+ }
+ else {
+ window->queueInfoPopup("ERROR: COULDN'T FIND APP '" + emulatorName +
+ "', HAS IT BEEN PROPERLY INSTALLED?",
+ 6000);
+ }
+ }
+ else {
+ if (emulatorName == "") {
+ window->queueInfoPopup(
+ "ERROR: COULDN'T FIND EMULATOR, HAS IT BEEN PROPERLY INSTALLED?", 6000);
+ }
+ else {
+ window->queueInfoPopup("ERROR: COULDN'T FIND EMULATOR '" + emulatorName +
+ "', HAS IT BEEN PROPERLY INSTALLED?",
+ 6000);
+ }
+ }
window->setAllowTextScrolling(true);
window->setAllowFileAnimation(true);
@@ -1473,18 +1574,30 @@ void FileData::launchGame()
injectFile = Utils::String::replace(injectFile, "\\", "/");
injectFile = Utils::String::replace(injectFile, "%BASENAME%",
Utils::String::replace(baseName, "\"", ""));
- if (injectFile.size() < 3 || !(injectFile[1] == ':' && injectFile[2] == '/'))
- injectFile =
- Utils::FileSystem::getParent(Utils::String::replace(romPath, "\"", "")) + "/" +
- injectFile;
+ if (injectFile == "%ROM%") {
+ injectFile = Utils::String::replace(injectFile, "%ROM%",
+ Utils::String::replace(romRaw, "\"", ""));
+ }
+ else {
+ if (injectFile.size() < 3 || !(injectFile[1] == ':' && injectFile[2] == '/'))
+ injectFile =
+ Utils::FileSystem::getParent(Utils::String::replace(romPath, "\"", "")) +
+ "/" + injectFile;
+ }
injectFile = Utils::String::replace(injectFile, "/", "\\");
#else
injectFile = Utils::String::replace(injectFile, "%BASENAME%",
Utils::String::replace(baseName, "\\", ""));
- if (injectFile.front() != '/')
- injectFile =
- Utils::FileSystem::getParent(Utils::String::replace(romPath, "\\", "")) + "/" +
- injectFile;
+ if (injectFile == "%ROM%") {
+ injectFile = Utils::String::replace(injectFile, "%ROM%",
+ Utils::String::replace(romRaw, "\\", ""));
+ }
+ else {
+ if (injectFile.front() != '/')
+ injectFile =
+ Utils::FileSystem::getParent(Utils::String::replace(romPath, "\\", "")) +
+ "/" + injectFile;
+ }
#endif
if (Utils::FileSystem::isRegularFile(injectFile) ||
Utils::FileSystem::isSymlink(injectFile)) {
@@ -1492,9 +1605,18 @@ void FileData::launchGame()
<< "\"";
std::string arguments;
std::ifstream injectFileStream;
+#if defined(_WIN64)
+ injectFileStream.open(Utils::String::stringToWideString(injectFile));
+#else
injectFileStream.open(injectFile);
- for (std::string line; getline(injectFileStream, line);)
+#endif
+ for (std::string line; getline(injectFileStream, line);) {
+ // Remove Windows carriage return characters.
+ line = Utils::String::replace(line, "\r", "");
arguments += line;
+ if (arguments.size() > 4096)
+ break;
+ }
injectFileStream.close();
if (arguments.empty()) {
@@ -1504,8 +1626,7 @@ void FileData::launchGame()
}
else if (arguments.size() > 4096) {
LOG(LogWarning) << "FileData::launchGame(): Injection file exceeding maximum "
- "allowed size of "
- "4096 bytes, skipping \""
+ "allowed size of 4096 bytes, skipping \""
<< injectFile << "\"";
}
else {
@@ -1634,6 +1755,10 @@ void FileData::launchGame()
Utils::FileSystem::getEscapedPath(getROMDirectory()));
#else
command = Utils::String::replace(command, "%ANDROIDPACKAGE%", androidPackage);
+ // Escaped quotation marks should only be used for Extras on Android so it should be safe to
+ // just change them to temporary variables and convert them back to the escaped quotation
+ // marks when parsing the Extras.
+ command = Utils::String::replace(command, "\\\"", "%QUOTATION%");
const std::vector androidVariabels {
"%ACTION%=", "%CATEGORY%=", "%MIMETYPE%=", "%DATA%="};
@@ -1741,6 +1866,21 @@ void FileData::launchGame()
}
if (extraName != "" && extraValue != "") {
+ // Expand the unescaped game directory path and ROM directory as well as
+ // the raw path to the game file if the corresponding variables have been
+ // used in the Extra definition. We also change back any temporary quotation
+ // mark variables to actual escaped quotation marks so they can be passed
+ // in the Intent.
+ extraValue = Utils::String::replace(extraValue, "%QUOTATION%", "\\\"");
+ extraValue =
+ Utils::String::replace(extraValue, "%GAMEDIRRAW%",
+ Utils::FileSystem::getParent(
+ Utils::String::replace(romPath, "\\", "")));
+ extraValue =
+ Utils::String::replace(extraValue, "%ROMPATHRAW%", getROMDirectory());
+ extraValue = Utils::String::replace(extraValue, "%ROMRAW%", romRaw);
+ extraValue = Utils::String::replace(extraValue, "//", "/");
+
if (variable == "%EXTRA_")
androidExtrasString[extraName] = extraValue;
else if (variable == "%EXTRAARRAY_")
@@ -1791,6 +1931,10 @@ void FileData::launchGame()
// Trim any leading and trailing whitespace characters as they could cause launch issues.
command = Utils::String::trim(command);
+#if defined(DEINIT_ON_LAUNCH)
+ runInBackground = false;
+#endif
+
// swapBuffers() is called here to turn the screen black to eliminate some potential
// flickering and to avoid showing the game launch message briefly when returning
// from the game.
@@ -1862,7 +2006,18 @@ void FileData::launchGame()
androidData, mEnvData->mStartPath, romRaw, androidExtrasString, androidExtrasStringArray,
androidExtrasBool, androidActivityFlags);
#else
+
+#if defined(DEINIT_ON_LAUNCH)
+// Deinit both the AudioManager and the window which allows emulators to launch in KMS mode.
+AudioManager::getInstance().deinit();
+window->deinit();
+returnValue = Utils::Platform::launchGameUnix(command, startDirectory, false);
+AudioManager::getInstance().init();
+window->init();
+#else
returnValue = Utils::Platform::launchGameUnix(command, startDirectory, runInBackground);
+#endif
+
#endif
// Notify the user in case of a failed game launch using a popup window.
if (returnValue != 0) {
diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h
index a12e5613d..23d654a64 100644
--- a/es-app/src/FileData.h
+++ b/es-app/src/FileData.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// FileData.h
//
// Provides game file data structures and functions to access and sort this information.
diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp
index 70da20e2d..358891110 100644
--- a/es-app/src/guis/GuiMenu.cpp
+++ b/es-app/src/guis/GuiMenu.cpp
@@ -40,6 +40,7 @@
#if defined(__ANDROID__)
#include "InputOverlay.h"
+#include "utils/PlatformUtilAndroid.h"
#endif
#include
@@ -80,6 +81,9 @@ GuiMenu::GuiMenu()
Settings::getInstance()->getString("UIMode") != "kiosk") {
#if defined(__APPLE__)
addEntry("QUIT RETRODECK", mMenuColorPrimary, false, [this] { openQuitMenu(); });
+#elif defined(__ANDROID__)
+ if (!AndroidVariables::sIsHomeApp)
+ addEntry("QUIT RETRODECK", mMenuColorPrimary, false, [this] { openQuitMenu(); });
#else
if (Settings::getInstance()->getBool("ShowQuitMenu"))
addEntry("QUIT", mMenuColorPrimary, true, [this] { openQuitMenu(); });
@@ -1655,7 +1659,7 @@ void GuiMenu::openOtherOptions()
});
#endif
-#if !defined(__ANDROID__)
+#if !defined(__ANDROID__) && !defined(DEINIT_ON_LAUNCH)
// Run ES in the background when a game has been launched.
auto runInBackground = std::make_shared();
runInBackground->setState(Settings::getInstance()->getBool("RunInBackground"));
@@ -1792,6 +1796,34 @@ void GuiMenu::openOtherOptions()
});
#endif
+#if defined(__ANDROID__)
+ if (!AndroidVariables::sIsHomeApp) {
+ // Whether swiping or pressing back should exit the application.
+ auto backEventAppExit = std::make_shared();
+ backEventAppExit->setState(Settings::getInstance()->getBool("BackEventAppExit"));
+ s->addWithLabel("BACK BUTTON/BACK SWIPE EXITS APP", backEventAppExit);
+ s->addSaveFunc([backEventAppExit, s] {
+ if (backEventAppExit->getState() !=
+ Settings::getInstance()->getBool("BackEventAppExit")) {
+ Settings::getInstance()->setBool("BackEventAppExit", backEventAppExit->getState());
+ s->setNeedsSaving();
+ }
+ });
+ }
+ else {
+ // If we're running as the Android home app then we don't allow the application to quit,
+ // so simply add a disabled dummy switch in this case.
+ auto backEventAppExit = std::make_shared();
+ s->addWithLabel("BACK BUTTON/BACK SWIPE EXITS APP", backEventAppExit);
+ backEventAppExit->setEnabled(false);
+ backEventAppExit->setState(false);
+ backEventAppExit->setOpacity(DISABLED_OPACITY);
+ backEventAppExit->getParent()
+ ->getChild(backEventAppExit->getChildIndex() - 1)
+ ->setOpacity(DISABLED_OPACITY);
+ }
+#endif
+
if (Settings::getInstance()->getBool("DebugFlag")) {
// If the --debug command line option was passed then create a dummy entry.
auto debugMode = std::make_shared();
diff --git a/es-app/src/guis/GuiOrphanedDataCleanup.cpp b/es-app/src/guis/GuiOrphanedDataCleanup.cpp
index b925bf649..b30943614 100644
--- a/es-app/src/guis/GuiOrphanedDataCleanup.cpp
+++ b/es-app/src/guis/GuiOrphanedDataCleanup.cpp
@@ -22,7 +22,6 @@ GuiOrphanedDataCleanup::GuiOrphanedDataCleanup(std::function reloadCallb
, mGrid {glm::ivec2 {4, 11}}
, mReloadCallback {reloadCallback}
, mCursorPos {0}
- , mMediaDirectory {FileData::getMediaDirectory()}
, mMediaTypes {"3dboxes", "backcovers", "covers", "fanart",
"manuals", "marquees", "miximages", "physicalmedia",
"screenshots", "titlescreens", "videos"}
@@ -36,6 +35,23 @@ GuiOrphanedDataCleanup::GuiOrphanedDataCleanup(std::function reloadCallb
, mCaseSensitiveFilesystem {true}
, mCleanupType {CleanupType::MEDIA}
{
+ // Make sure we always have a single trailing directory separator for the media directory.
+ mMediaDirectory = FileData::getMediaDirectory();
+ mMediaDirectory.erase(std::find_if(mMediaDirectory.rbegin(), mMediaDirectory.rend(),
+ [](char c) { return c != '/'; })
+ .base(),
+ mMediaDirectory.end());
+
+ mMediaDirectory.erase(std::find_if(mMediaDirectory.rbegin(), mMediaDirectory.rend(),
+ [](char c) { return c != '\\'; })
+ .base(),
+ mMediaDirectory.end());
+#if defined(_WIN64)
+ mMediaDirectory.append("\\");
+#else
+ mMediaDirectory.append("/");
+#endif
+
addChild(&mBackground);
addChild(&mGrid);
diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp
index 013042cdb..09d42f0dc 100644
--- a/es-app/src/guis/GuiSettings.cpp
+++ b/es-app/src/guis/GuiSettings.cpp
@@ -112,7 +112,7 @@ void GuiSettings::save()
ViewController::getInstance()->reloadAll();
if (mNeedsGoToStart)
- ViewController::getInstance()->goToStart(true);
+ ViewController::getInstance()->goToStart(false);
// Special case from GuiCollectionSystemsOptions where we didn't yet know whether a matching
// theme existed when creating a new custom collection.
diff --git a/es-app/src/guis/GuiThemeDownloader.cpp b/es-app/src/guis/GuiThemeDownloader.cpp
index af850c6f8..a5f597035 100644
--- a/es-app/src/guis/GuiThemeDownloader.cpp
+++ b/es-app/src/guis/GuiThemeDownloader.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// GuiThemeDownloader.cpp
//
// Theme downloader.
@@ -395,7 +395,9 @@ bool GuiThemeDownloader::fetchRepository(const std::string& repositoryName, bool
LOG(LogError) << "GuiThemeDownloader: " << runtimeError.what() << gitError->message;
mRepositoryError = RepositoryError::FETCH_ERROR;
mMessage = gitError->message;
+#if LIBGIT2_VER_MAJOR < 2 && LIBGIT2_VER_MINOR < 8
git_error_clear();
+#endif
git_remote_free(gitRemote);
git_repository_free(repository);
mPromise.set_value(true);
@@ -663,6 +665,9 @@ void GuiThemeDownloader::parseThemesList()
if (theme.HasMember("newEntry") && theme["newEntry"].IsBool())
themeEntry.newEntry = theme["newEntry"].GetBool();
+ if (theme.HasMember("deprecated") && theme["deprecated"].IsBool())
+ themeEntry.deprecated = theme["deprecated"].GetBool();
+
if (theme.HasMember("variants") && theme["variants"].IsArray()) {
const rapidjson::Value& variants {theme["variants"]};
for (int i {0}; i < static_cast(variants.Size()); ++i)
@@ -746,6 +751,11 @@ void GuiThemeDownloader::populateGUI()
std::shared_ptr themeNameElement {std::make_shared(
themeName, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)};
+ if (theme.deprecated)
+ themeNameElement->setOpacity(0.4f);
+ else
+ themeNameElement->setOpacity(1.0f);
+
ThemeGUIEntry guiEntry;
guiEntry.themeName = themeNameElement;
mThemeGUIEntries.emplace_back(guiEntry);
@@ -907,11 +917,21 @@ void GuiThemeDownloader::updateGUI()
void GuiThemeDownloader::updateInfoPane()
{
assert(static_cast(mList->size()) == mThemes.size());
- if (!mThemes[mList->getCursorId()].screenshots.empty())
+ if (!mThemes[mList->getCursorId()].screenshots.empty()) {
mScreenshot->setImage(mThemeDirectory + "themes-list/" +
mThemes[mList->getCursorId()].screenshots.front().image);
- else
+ if (mThemes[mList->getCursorId()].deprecated) {
+ mScreenshot->setSaturation(0.0f);
+ mScreenshot->setBrightness(-0.2f);
+ }
+ else {
+ mScreenshot->setSaturation(1.0f);
+ mScreenshot->setBrightness(0.0f);
+ }
+ }
+ else {
mScreenshot->setImage("");
+ }
if (mThemes[mList->getCursorId()].isCloned) {
mDownloadStatus->setText(ViewController::TICKMARK_CHAR + " INSTALLED");
@@ -954,7 +974,11 @@ void GuiThemeDownloader::updateInfoPane()
mColorSchemesCount->setText(std::to_string(mThemes[mList->getCursorId()].colorSchemes.size()));
mAspectRatiosCount->setText(std::to_string(mThemes[mList->getCursorId()].aspectRatios.size()));
mFontSizesCount->setText(std::to_string(mThemes[mList->getCursorId()].fontSizes.size()));
- mAuthor->setText("CREATED BY " + Utils::String::toUpper(mThemes[mList->getCursorId()].author));
+ if (mThemes[mList->getCursorId()].deprecated)
+ mAuthor->setText("THIS THEME ENTRY WILL BE REMOVED IN THE NEAR FUTURE");
+ else
+ mAuthor->setText("CREATED BY " +
+ Utils::String::toUpper(mThemes[mList->getCursorId()].author));
}
void GuiThemeDownloader::setupFullscreenViewer()
@@ -1419,7 +1443,9 @@ bool GuiThemeDownloader::cloneRepository(const std::string& repositoryName, cons
<< gitError->message << "\"";
mRepositoryError = RepositoryError::CLONE_ERROR;
mMessage = gitError->message;
+#if LIBGIT2_VER_MAJOR < 2 && LIBGIT2_VER_MINOR < 8
git_error_clear();
+#endif
mPromise.set_value(true);
return true;
}
diff --git a/es-app/src/guis/GuiThemeDownloader.h b/es-app/src/guis/GuiThemeDownloader.h
index 2a807756e..f56a2fe68 100644
--- a/es-app/src/guis/GuiThemeDownloader.h
+++ b/es-app/src/guis/GuiThemeDownloader.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// GuiThemeDownloader.h
//
// Theme downloader.
@@ -68,6 +68,7 @@ private:
std::vector transitions;
std::vector screenshots;
bool newEntry;
+ bool deprecated;
bool invalidRepository;
bool corruptRepository;
bool shallowRepository;
@@ -76,6 +77,7 @@ private:
bool isCloned;
ThemeEntry()
: newEntry {false}
+ , deprecated {false}
, invalidRepository {false}
, corruptRepository {false}
, shallowRepository {false}
diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp
index a95f62e42..018f5aa4e 100644
--- a/es-app/src/main.cpp
+++ b/es-app/src/main.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE is a frontend for browsing and launching games from your multi-platform game collection.
+// ES-DE is a frontend for browsing and launching games from your multi-platform collection.
//
// The column limit is 100 characters.
// All ES-DE C++ source code is formatted using clang-format.
@@ -558,6 +558,8 @@ int main(int argc, char* argv[])
std::locale::global(std::locale("C"));
+ SDL_SetHint(SDL_HINT_APP_NAME, "ES-DE");
+
#if defined(__APPLE__)
// This is a workaround to disable the incredibly annoying save state functionality in
// macOS which forces a restore of the previous window state. The problem is that this
@@ -610,17 +612,15 @@ int main(int argc, char* argv[])
// unusable in any emulator that is launched.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "0");
- bool resetTouchOverlay {false};
-
// If ES-DE is set as the home app/launcher we may be in a situation where we get started
// before the external storage has been mounted. If the application data directory or the
// ROMs directory have been located on this storage then the configurator will get executed.
- // To prevent the likelyhood of this happening we wait up to 40 * 100 milliseconds, then
+ // To prevent the likelyhood of this happening we wait up to 45 * 100 milliseconds, then
// we give up. This is not an airtight solution but it hopefully decreases the risk of
// this failure occuring. Under normal circumstances the storage would be mounted when
// the application is starting, so no delay would occur.
if (SDL_AndroidGetExternalStorageState() == 0) {
- for (int i {0}; i < 40; ++i) {
+ for (int i {0}; i < 45; ++i) {
__android_log_print(ANDROID_LOG_VERBOSE, ANDROID_APPLICATION_ID,
"Storage not mounted, waiting 100 ms until next attempt");
SDL_Delay(100);
@@ -637,9 +637,6 @@ int main(int argc, char* argv[])
if (Utils::Platform::Android::checkConfigurationNeeded())
exit(0);
-
- // Always enable the touch overlay after running the configurator.
- resetTouchOverlay = true;
}
Utils::Platform::Android::setDataDirectories();
@@ -701,6 +698,14 @@ int main(int argc, char* argv[])
LOG(LogInfo) << applicationName << " " << PROGRAM_VERSION_STRING << "-"
<< ANDROID_VERSION_CODE << " (r" << PROGRAM_RELEASE_NUMBER << "), built "
<< PROGRAM_BUILT_STRING;
+
+ if (AndroidVariables::sIsHomeApp) {
+ LOG(LogInfo) << "Running as the Android home app";
+ }
+ else {
+ LOG(LogInfo) << "Running as a regular Android app";
+ }
+
#else
LOG(LogInfo) << applicationName << " " << PROGRAM_VERSION_STRING << " (r"
<< PROGRAM_RELEASE_NUMBER << "), built " << PROGRAM_BUILT_STRING;
@@ -744,7 +749,12 @@ int main(int argc, char* argv[])
// Create the settings folder in the application data directory.
const std::string settingsDir {Utils::FileSystem::getAppDataDirectory() + "/settings"};
if (!Utils::FileSystem::isDirectory(settingsDir)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating settings directory \""
+ << Utils::String::replace(settingsDir, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating settings directory \"" << settingsDir << "\"...";
+#endif
Utils::FileSystem::createDirectory(settingsDir);
if (!Utils::FileSystem::isDirectory(settingsDir)) {
LOG(LogError) << "Couldn't create directory, permission problems?";
@@ -790,7 +800,8 @@ int main(int argc, char* argv[])
}
#if defined(__ANDROID__)
- if (resetTouchOverlay) {
+ // Reset the touch overlay if at least the second screen of the configurator was reached.
+ if (AndroidVariables::sResetTouchOverlay) {
Settings::getInstance()->setBool("InputTouchOverlay", true);
Settings::getInstance()->saveFile();
}
@@ -819,7 +830,12 @@ int main(int argc, char* argv[])
// Create the gamelists folder in the application data directory.
const std::string gamelistsDir {Utils::FileSystem::getAppDataDirectory() + "/gamelists"};
if (!Utils::FileSystem::exists(gamelistsDir)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating gamelists directory \""
+ << Utils::String::replace(gamelistsDir, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating gamelists directory \"" << gamelistsDir << "\"...";
+#endif
Utils::FileSystem::createDirectory(gamelistsDir);
if (!Utils::FileSystem::exists(gamelistsDir)) {
LOG(LogWarning) << "Couldn't create directory, permission problems?";
@@ -827,6 +843,23 @@ int main(int argc, char* argv[])
}
}
+ {
+ // Create the game media folder.
+ const std::string mediaDirectory {FileData::getMediaDirectory()};
+ if (!Utils::FileSystem::exists(mediaDirectory)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating game media directory \""
+ << Utils::String::replace(mediaDirectory, "/", "\\") << "\"...";
+#else
+ LOG(LogInfo) << "Creating game media directory \"" << mediaDirectory << "\"...";
+#endif
+ Utils::FileSystem::createDirectory(mediaDirectory);
+ if (!Utils::FileSystem::exists(mediaDirectory)) {
+ LOG(LogWarning) << "Couldn't create directory, permission problems?";
+ }
+ }
+ }
+
{
#if defined(__ANDROID__)
const std::string themeDir {Utils::FileSystem::getAppDataDirectory() + "/themes"};
@@ -861,8 +894,12 @@ int main(int argc, char* argv[])
userThemeDirectory = userThemeDirSetting;
if (!Utils::FileSystem::exists(userThemeDirectory)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating themes directory \""
+ << Utils::String::replace(userThemeDirectory, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating themes directory \"" << userThemeDirectory << "\"...";
-
+#endif
Utils::FileSystem::createDirectory(userThemeDirectory);
if (!Utils::FileSystem::exists(userThemeDirectory)) {
LOG(LogWarning) << "Couldn't create directory, permission problems?";
@@ -891,7 +928,12 @@ int main(int argc, char* argv[])
// for custom event scripts so it's also created as a convenience.
const std::string scriptsDir {Utils::FileSystem::getAppDataDirectory() + "/scripts"};
if (!Utils::FileSystem::exists(scriptsDir)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating scripts directory \""
+ << Utils::String::replace(scriptsDir, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating scripts directory \"" << scriptsDir << "\"...";
+#endif
Utils::FileSystem::createDirectory(scriptsDir);
if (!Utils::FileSystem::exists(scriptsDir)) {
LOG(LogWarning) << "Couldn't create directory, permission problems?";
@@ -906,7 +948,12 @@ int main(int argc, char* argv[])
const std::string slideshowDir {Utils::FileSystem::getAppDataDirectory() +
"/screensavers/custom_slideshow"};
if (!Utils::FileSystem::exists(screensaversDir)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating screensavers directory \""
+ << Utils::String::replace(screensaversDir, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating screensavers directory \"" << screensaversDir << "\"...";
+#endif
Utils::FileSystem::createDirectory(screensaversDir);
if (!Utils::FileSystem::exists(screensaversDir)) {
LOG(LogWarning) << "Couldn't create directory, permission problems?";
@@ -923,7 +970,12 @@ int main(int argc, char* argv[])
}
#endif
if (!Utils::FileSystem::exists(slideshowDir)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating custom_slideshow directory \""
+ << Utils::String::replace(slideshowDir, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating custom_slideshow directory \"" << slideshowDir << "\"...";
+#endif
Utils::FileSystem::createDirectory(slideshowDir);
if (!Utils::FileSystem::exists(slideshowDir)) {
LOG(LogWarning) << "Couldn't create directory, permission problems?";
@@ -937,7 +989,12 @@ int main(int argc, char* argv[])
const std::string controllersDir {Utils::FileSystem::getAppDataDirectory() +
"/controllers"};
if (!Utils::FileSystem::exists(controllersDir)) {
+#if defined(_WIN64)
+ LOG(LogInfo) << "Creating controllers directory \""
+ << Utils::String::replace(controllersDir, "/", "\\") << "\"...";
+#else
LOG(LogInfo) << "Creating controllers directory \"" << controllersDir << "\"...";
+#endif
Utils::FileSystem::createDirectory(controllersDir);
if (!Utils::FileSystem::exists(controllersDir)) {
LOG(LogWarning) << "Couldn't create directory, permission problems?";
@@ -1121,6 +1178,8 @@ int main(int argc, char* argv[])
#if defined(APPLICATION_UPDATER)
if (ApplicationUpdater::getInstance().getResults())
ViewController::getInstance()->updateAvailableDialog();
+ else
+ HttpReq::cleanupCurlMulti();
#endif
#if defined(_WIN64)
diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp
index 03d7dc8ad..a36807094 100644
--- a/es-app/src/views/ViewController.cpp
+++ b/es-app/src/views/ViewController.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// ViewController.cpp
//
// Handles overall system navigation including animations and transitions.
@@ -139,11 +139,22 @@ void ViewController::setMenuColors()
void ViewController::legacyAppDataDialog()
{
- const std::string upgradeMessage {
+ const std::string upgradeMessage
+ {
"AS OF ES-DE 3.0 THE APPLICATION DATA DIRECTORY HAS CHANGED FROM \".emulationstation\" "
"to \"ES-DE\"\nPLEASE RENAME YOUR CURRENT DATA DIRECTORY:\n" +
- Utils::FileSystem::getAppDataDirectory() + "\nTO THE FOLLOWING:\n" +
- Utils::FileSystem::getParent(Utils::FileSystem::getAppDataDirectory()) + "/ES-DE"};
+#if defined(_WIN64)
+ Utils::String::replace(Utils::FileSystem::getAppDataDirectory(), "/", "\\") +
+ "\nTO THE FOLLOWING:\n" +
+ Utils::String::replace(
+ Utils::FileSystem::getParent(Utils::FileSystem::getAppDataDirectory()), "/", "\\") +
+ "\\ES-DE"
+ };
+#else
+ Utils::FileSystem::getAppDataDirectory() + "\nTO THE FOLLOWING:\n" +
+ Utils::FileSystem::getParent(Utils::FileSystem::getAppDataDirectory()) + "/ES-DE"
+ };
+#endif
mWindow->pushGui(new GuiMsgBox(
HelpStyle(), upgradeMessage.c_str(), "OK", [] {}, "", nullptr, "", nullptr, nullptr, true,
@@ -317,7 +328,12 @@ void ViewController::updateAvailableDialog()
0.535f * (1.778f / mRenderer->getScreenAspectRatio()))));
}
},
- "CANCEL", [] { return; }, "", nullptr, nullptr, true, true,
+ "CANCEL",
+ [] {
+ HttpReq::cleanupCurlMulti();
+ return;
+ },
+ "", nullptr, nullptr, true, true,
(mRenderer->getIsVerticalOrientation() ?
0.70f :
0.45f * (1.778f / mRenderer->getScreenAspectRatio()))));
diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h
index 68845b9b7..d24a6d383 100644
--- a/es-app/src/views/ViewController.h
+++ b/es-app/src/views/ViewController.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// ViewController.h
//
// Handles overall system navigation including animations and transitions.
diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt
index e4c387850..6222898fb 100644
--- a/es-core/CMakeLists.txt
+++ b/es-core/CMakeLists.txt
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: MIT
#
-# ES-DE
+# ES-DE Frontend
# CMakeLists.txt (es-core)
#
# CMake configuration for es-core
@@ -9,6 +9,7 @@
project(core)
set(CORE_HEADERS
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationVersion.h
${CMAKE_CURRENT_SOURCE_DIR}/src/AsyncHandle.h
${CMAKE_CURRENT_SOURCE_DIR}/src/AudioManager.h
${CMAKE_CURRENT_SOURCE_DIR}/src/CECInput.h
diff --git a/es-app/src/ApplicationVersion.h b/es-core/src/ApplicationVersion.h
similarity index 62%
rename from es-app/src/ApplicationVersion.h
rename to es-core/src/ApplicationVersion.h
index a8d3bc264..f3491d6b0 100644
--- a/es-app/src/ApplicationVersion.h
+++ b/es-core/src/ApplicationVersion.h
@@ -1,25 +1,25 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// ApplicationVersion.h
//
-#ifndef ES_APP_APPLICATION_VERSION_H
-#define ES_APP_APPLICATION_VERSION_H
+#ifndef ES_CORE_APPLICATION_VERSION_H
+#define ES_CORE_APPLICATION_VERSION_H
// These numbers and strings need to be manually updated for a new version.
// Do this version number update as the very last commit for the new release version.
// clang-format off
#define PROGRAM_VERSION_MAJOR 3
#define PROGRAM_VERSION_MINOR 0
-#define PROGRAM_VERSION_MAINTENANCE 1
-#define PROGRAM_RELEASE_NUMBER 42
+#define PROGRAM_VERSION_MAINTENANCE 3
+#define PROGRAM_RELEASE_NUMBER 44
// clang-format on
-#define PROGRAM_VERSION_STRING "3.0.1"
+#define PROGRAM_VERSION_STRING "3.0.3"
#define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__
-#define RESOURCE_VERSION_STRING "3,0,1\0"
+#define RESOURCE_VERSION_STRING "3,0,3\0"
#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR, PROGRAM_VERSION_MINOR, PROGRAM_VERSION_MAINTENANCE
-#endif // ES_APP_APPLICATION_VERSION_H
+#endif // ES_CORE_APPLICATION_VERSION_H
diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp
index 0cf5d023e..e3f38e1b0 100644
--- a/es-core/src/AudioManager.cpp
+++ b/es-core/src/AudioManager.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// AudioManager.cpp
//
// Low-level audio functions (using SDL2).
@@ -113,6 +113,9 @@ void AudioManager::init()
void AudioManager::deinit()
{
+ if (sAudioDevice == 0)
+ return;
+
SDL_LockAudioDevice(sAudioDevice);
SDL_FreeAudioStream(sConversionStream);
SDL_UnlockAudioDevice(sAudioDevice);
@@ -120,6 +123,7 @@ void AudioManager::deinit()
SDL_CloseAudio();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
+ sConversionStream = nullptr;
sAudioDevice = 0;
}
@@ -132,7 +136,7 @@ void AudioManager::mixAudio(void* /*unused*/, Uint8* stream, int len)
SDL_memset(stream, 0, len);
// Iterate through all our samples.
- std::vector>::const_iterator soundIt = sSoundVector.cbegin();
+ std::vector>::const_iterator soundIt {sSoundVector.cbegin()};
while (soundIt != sSoundVector.cend()) {
std::shared_ptr sound {*soundIt};
if (sound->isPlaying()) {
diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h
index d274eb095..bc0ffb497 100644
--- a/es-core/src/AudioManager.h
+++ b/es-core/src/AudioManager.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// AudioManager.h
//
// Low-level audio functions (using SDL2).
@@ -49,10 +49,10 @@ private:
static void mixAudio(void* unused, Uint8* stream, int len);
- static inline SDL_AudioStream* sConversionStream;
+ static inline SDL_AudioStream* sConversionStream {nullptr};
static inline std::vector> sSoundVector;
- static inline std::atomic sMuteStream = false;
- static inline bool sHasAudioDevice = true;
+ static inline std::atomic sMuteStream {false};
+ static inline bool sHasAudioDevice {true};
};
#endif // ES_CORE_AUDIO_MANAGER_H
diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp
index 73fd81ead..305bbad0d 100644
--- a/es-core/src/HttpReq.cpp
+++ b/es-core/src/HttpReq.cpp
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// HttpReq.cpp
//
// HTTP requests using libcurl.
@@ -9,11 +9,13 @@
#include "HttpReq.h"
+#include "ApplicationVersion.h"
#include "Log.h"
#include "Settings.h"
#include "resources/ResourceManager.h"
#include "utils/FileSystemUtil.h"
+#include
#include
std::string HttpReq::urlEncode(const std::string& s)
@@ -37,42 +39,41 @@ std::string HttpReq::urlEncode(const std::string& s)
}
HttpReq::HttpReq(const std::string& url, bool scraperRequest)
- : mStatus(REQ_IN_PROGRESS)
- , mHandle(nullptr)
+ : mStatus {REQ_IN_PROGRESS}
+ , mHandle {nullptr}
, mTotalBytes {0}
, mDownloadedBytes {0}
, mScraperRequest {scraperRequest}
{
- // The multi-handle is cleaned up via a call from GuiScraperSearch after the scraping
- // has been completed for a game, meaning the handle is valid for all curl requests
- // performed for the current game.
+ // The multi-handle is cleaned up via an explicit call to cleanupCurlMulti() from any object
+ // that uses HttpReq. For example from GuiScraperSearch after scraping has been completed.
if (!sMultiHandle)
sMultiHandle = curl_multi_init();
mHandle = curl_easy_init();
-#if defined(USE_BUNDLED_CERTIFICATES)
- // Use the bundled curl TLS/SSL certificates (which actually come from the Mozilla project).
- // This is enabled by default on Windows. Although there is a possibility to use the OS
- // provided Schannel certificates I haven't been able to get this to work, and it also seems
- // to be problematic on older Windows versions.
- // The bundled certificates are also required on Linux when building an AppImage package as
- // distributions such as Debian, Ubuntu, Linux Mint and Manjaro place the TLS certificates in
- // a different directory than for example Fedora and openSUSE. This makes curl unusable on
- // these latter operating systems unless the bundled file is used.
- curl_easy_setopt(mHandle, CURLOPT_CAINFO,
- ResourceManager::getInstance()
- .getResourcePath(":/certificates/curl-ca-bundle.crt")
- .c_str());
-#endif
-
if (mHandle == nullptr) {
mStatus = REQ_IO_ERROR;
onError("curl_easy_init failed");
return;
}
- // Set the url.
+ if (!mPollThread) {
+ sStopPoll = false;
+ mPollThread = std::make_unique(&HttpReq::pollCurl, this);
+ }
+
+#if defined(USE_BUNDLED_CERTIFICATES)
+ // Use the bundled curl TLS/SSL certificates (which come from the Mozilla project).
+ // This is used on Windows and also on Android as there is no way for curl to access
+ // the system certificates on this OS.
+ curl_easy_setopt(mHandle, CURLOPT_CAINFO,
+ ResourceManager::getInstance()
+ .getResourcePath(":/certificates/curl-ca-bundle.crt")
+ .c_str());
+#endif
+
+ // Set the URL.
CURLcode err {curl_easy_setopt(mHandle, CURLOPT_URL, url.c_str())};
if (err != CURLE_OK) {
mStatus = REQ_IO_ERROR;
@@ -80,6 +81,32 @@ HttpReq::HttpReq(const std::string& url, bool scraperRequest)
return;
}
+ if (!mScraperRequest) {
+ // Set User-Agent.
+ std::string userAgent {"ES-DE Frontend/"};
+ userAgent.append(PROGRAM_VERSION_STRING).append(" (");
+#if defined(__ANDROID__)
+ userAgent.append("Android");
+#elif defined(_WIN64)
+ userAgent.append("Windows");
+#elif defined(__APPLE__)
+ userAgent.append("macOS");
+#elif defined(__linux__)
+ userAgent.append("Linux");
+#elif defined(__unix__)
+ userAgent.append("Unix");
+#else
+ userAgent.append("Unknown");
+#endif
+ userAgent.append(")");
+ CURLcode err {curl_easy_setopt(mHandle, CURLOPT_USERAGENT, userAgent.c_str())};
+ if (err != CURLE_OK) {
+ mStatus = REQ_IO_ERROR;
+ onError(curl_easy_strerror(err));
+ return;
+ }
+ }
+
long connectionTimeout;
if (mScraperRequest) {
@@ -172,7 +199,7 @@ HttpReq::HttpReq(const std::string& url, bool scraperRequest)
}
// Enable the curl progress meter.
- err = curl_easy_setopt(mHandle, CURLOPT_NOPROGRESS, 0);
+ err = curl_easy_setopt(mHandle, CURLOPT_NOPROGRESS, mScraperRequest ? 1 : 0);
if (err != CURLE_OK) {
mStatus = REQ_IO_ERROR;
onError(curl_easy_strerror(err));
@@ -188,11 +215,13 @@ HttpReq::HttpReq(const std::string& url, bool scraperRequest)
}
// Progress meter callback.
- err = curl_easy_setopt(mHandle, CURLOPT_XFERINFOFUNCTION, HttpReq::transferProgress);
- if (err != CURLE_OK) {
- mStatus = REQ_IO_ERROR;
- onError(curl_easy_strerror(err));
- return;
+ if (!mScraperRequest) {
+ err = curl_easy_setopt(mHandle, CURLOPT_XFERINFOFUNCTION, HttpReq::transferProgress);
+ if (err != CURLE_OK) {
+ mStatus = REQ_IO_ERROR;
+ onError(curl_easy_strerror(err));
+ return;
+ }
}
// Fail on HTTP status codes >= 400.
@@ -203,92 +232,33 @@ HttpReq::HttpReq(const std::string& url, bool scraperRequest)
return;
}
- // Add the handle to our multi.
- CURLMcode merr {curl_multi_add_handle(sMultiHandle, mHandle)};
- if (merr != CURLM_OK) {
- mStatus = REQ_IO_ERROR;
- onError(curl_multi_strerror(merr));
- return;
- }
+ // Add the handle to the multi. This is done in pollCurl(), running in a separate thread.
+ std::unique_lock handleLock {sHandleMutex};
+ sAddHandleQueue.push(mHandle);
+ handleLock.unlock();
+ curl_multi_wakeup(sMultiHandle);
+
+ std::unique_lock requestLock {sRequestMutex};
sRequests[mHandle] = this;
+ requestLock.unlock();
}
HttpReq::~HttpReq()
{
if (mHandle) {
+ std::unique_lock requestLock {sRequestMutex};
sRequests.erase(mHandle);
+ requestLock.unlock();
- CURLMcode merr {curl_multi_remove_handle(sMultiHandle, mHandle)};
+ std::unique_lock handleLock {sHandleMutex};
+ sRemoveHandleQueue.push(mHandle);
+ handleLock.unlock();
- if (merr != CURLM_OK) {
- LOG(LogError) << "Error removing curl_easy handle from curl_multi: "
- << curl_multi_strerror(merr);
- }
-
- curl_easy_cleanup(mHandle);
+ curl_multi_wakeup(sMultiHandle);
}
}
-HttpReq::Status HttpReq::status()
-{
- if (mStatus == REQ_IN_PROGRESS) {
- int handleCount {0};
- CURLMcode merr {curl_multi_perform(sMultiHandle, &handleCount)};
- if (merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) {
- mStatus = REQ_IO_ERROR;
- onError(curl_multi_strerror(merr));
- return mStatus;
- }
-
- int msgsLeft;
- CURLMsg* msg;
- while ((msg = curl_multi_info_read(sMultiHandle, &msgsLeft)) != nullptr) {
- if (msg->msg == CURLMSG_DONE) {
- HttpReq* req {sRequests[msg->easy_handle]};
-
- if (req == nullptr) {
- LOG(LogError) << "Cannot find easy handle!";
- continue;
- }
-
- if (msg->data.result == CURLE_OK) {
- req->mStatus = REQ_SUCCESS;
- }
- else if (msg->data.result == CURLE_PEER_FAILED_VERIFICATION) {
- req->mStatus = REQ_FAILED_VERIFICATION;
- req->onError(curl_easy_strerror(msg->data.result));
- }
- else if (msg->data.result == CURLE_HTTP_RETURNED_ERROR) {
- long responseCode;
- curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &responseCode);
-
- if (responseCode == 430 &&
- Settings::getInstance()->getString("Scraper") == "screenscraper") {
- req->mContent << "You have exceeded your daily scrape quota";
- req->mStatus = REQ_SUCCESS;
- }
- else if (responseCode == 404 && mScraperRequest &&
- Settings::getInstance()->getBool("ScraperIgnoreHTTP404Errors")) {
- req->mStatus = REQ_RESOURCE_NOT_FOUND;
- }
- else {
- req->onError("Server returned HTTP error code " +
- std::to_string(responseCode));
- req->mStatus = REQ_BAD_STATUS_CODE;
- }
- }
- else {
- req->mStatus = REQ_IO_ERROR;
- req->onError(curl_easy_strerror(msg->data.result));
- }
- }
- }
- }
-
- return mStatus;
-}
-
std::string HttpReq::getContent() const
{
assert(mStatus == REQ_SUCCESS);
@@ -298,21 +268,156 @@ std::string HttpReq::getContent() const
int HttpReq::transferProgress(
void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
- // Note that it's not guaranteed that the server will actually provide the total size.
- if (dltotal > 0)
- static_cast(clientp)->mTotalBytes = static_cast(dltotal);
- if (dlnow > 0)
- static_cast(clientp)->mDownloadedBytes = static_cast(dlnow);
+ if (dltotal == 0 && dlnow == 0)
+ return CURLE_OK;
+
+ // We need all the check logic below to make sure we're not attempting to write into
+ // a request that has just been removed by the main thread.
+ bool validEntry {false};
+
+ std::unique_lock requestLock {sRequestMutex};
+ if (std::find_if(sRequests.cbegin(), sRequests.cend(), [&clientp](auto&& entry) {
+ return entry.second == clientp;
+ }) != sRequests.cend())
+ validEntry = true;
+
+ if (validEntry) {
+ // Note that it's not guaranteed that the server will actually provide the total size.
+ if (dltotal > 0)
+ static_cast(clientp)->mTotalBytes = static_cast(dltotal);
+ if (dlnow > 0)
+ static_cast(clientp)->mDownloadedBytes = static_cast(dlnow);
+ }
+
+ requestLock.unlock();
return CURLE_OK;
}
size_t HttpReq::writeContent(void* buff, size_t size, size_t nmemb, void* req_ptr)
{
- // size = size of an element, nmemb = number of elements.
- std::stringstream& ss {static_cast(req_ptr)->mContent};
- ss.write(static_cast(buff), size * nmemb);
+ // We need all the check logic below to make sure we're not attempting to write into
+ // a request that has just been removed by the main thread.
+ bool validEntry {false};
+
+ std::unique_lock requestLock {sRequestMutex};
+ if (std::find_if(sRequests.cbegin(), sRequests.cend(), [&req_ptr](auto&& entry) {
+ return entry.second == req_ptr;
+ }) != sRequests.cend())
+ validEntry = true;
+
+ if (validEntry) {
+ // size = size of an element, nmemb = number of elements.
+ std::stringstream& ss {static_cast(req_ptr)->mContent};
+ ss.write(static_cast(buff), size * nmemb);
+ }
+
+ requestLock.unlock();
// Return value is number of elements successfully read.
return nmemb;
}
+
+void HttpReq::pollCurl()
+{
+ int numfds {0};
+
+ do {
+ if (!sStopPoll)
+ curl_multi_poll(sMultiHandle, nullptr, 0, 2000, &numfds);
+
+ // Check if any easy handles should be added or removed.
+ std::unique_lock handleLock {sHandleMutex};
+
+ if (sAddHandleQueue.size() > 0) {
+ // Add the handle to our multi.
+ CURLMcode merr {curl_multi_add_handle(sMultiHandle, sAddHandleQueue.front())};
+
+ std::unique_lock requestLock {sRequestMutex};
+ HttpReq* req {sRequests[sAddHandleQueue.front()]};
+ if (merr != CURLM_OK) {
+ if (req != nullptr) {
+ req->mStatus = REQ_IO_ERROR;
+ req->onError(curl_multi_strerror(merr));
+ LOG(LogError) << "onError(): " << curl_multi_strerror(merr);
+ }
+ }
+ else {
+ if (req != nullptr)
+ req->mStatus = REQ_IN_PROGRESS;
+ }
+ sAddHandleQueue.pop();
+ requestLock.unlock();
+ }
+
+ if (sRemoveHandleQueue.size() > 0) {
+ // Remove the handle from our multi.
+ CURLMcode merr {curl_multi_remove_handle(sMultiHandle, sRemoveHandleQueue.front())};
+ if (merr != CURLM_OK) {
+ LOG(LogError) << "Error removing curl easy handle from curl multi: "
+ << curl_multi_strerror(merr);
+ }
+ curl_easy_cleanup(sRemoveHandleQueue.front());
+ sRemoveHandleQueue.pop();
+ }
+
+ handleLock.unlock();
+
+ if (sMultiHandle != nullptr && !sStopPoll) {
+ int handleCount {0};
+ std::unique_lock handleLock {sHandleMutex};
+ CURLMcode merr {curl_multi_perform(sMultiHandle, &handleCount)};
+ handleLock.unlock();
+ if (merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) {
+ LOG(LogError) << "Error reading data from multi: " << curl_multi_strerror(merr);
+ }
+
+ int msgsLeft;
+ CURLMsg* msg;
+ while (!sStopPoll && (msg = curl_multi_info_read(sMultiHandle, &msgsLeft)) != nullptr) {
+ if (msg->msg == CURLMSG_DONE) {
+ std::unique_lock requestLock {sRequestMutex};
+ HttpReq* req {sRequests[msg->easy_handle]};
+
+ if (req == nullptr) {
+ LOG(LogError) << "Cannot find easy handle!";
+ requestLock.unlock();
+ continue;
+ }
+
+ if (msg->data.result == CURLE_OK) {
+ req->mStatus = REQ_SUCCESS;
+ }
+ else if (msg->data.result == CURLE_PEER_FAILED_VERIFICATION) {
+ req->mStatus = REQ_FAILED_VERIFICATION;
+ req->onError(curl_easy_strerror(msg->data.result));
+ }
+ else if (msg->data.result == CURLE_HTTP_RETURNED_ERROR) {
+ long responseCode;
+ curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &responseCode);
+
+ if (responseCode == 430 &&
+ Settings::getInstance()->getString("Scraper") == "screenscraper") {
+ req->mContent << "You have exceeded your daily scrape quota";
+ req->mStatus = REQ_SUCCESS;
+ }
+ else if (responseCode == 404 && req->mScraperRequest &&
+ Settings::getInstance()->getBool("ScraperIgnoreHTTP404Errors")) {
+ req->mStatus = REQ_RESOURCE_NOT_FOUND;
+ }
+ else {
+ req->mStatus = REQ_BAD_STATUS_CODE;
+ req->onError("Server returned HTTP error code " +
+ std::to_string(responseCode));
+ }
+ }
+ else {
+ req->mStatus = REQ_IO_ERROR;
+ req->onError(curl_easy_strerror(msg->data.result));
+ }
+ requestLock.unlock();
+ }
+ }
+ }
+ } while (!sStopPoll || !sAddHandleQueue.empty() || !sRemoveHandleQueue.empty());
+}
diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h
index 0de59915a..24f9489a9 100644
--- a/es-core/src/HttpReq.h
+++ b/es-core/src/HttpReq.h
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: MIT
//
-// ES-DE
+// ES-DE Frontend
// HttpReq.h
//
// HTTP requests using libcurl.
@@ -14,7 +14,10 @@
#include
#include