og-edopro/gframe/utils.cpp
Edoardo Lolletti 8d6d01c666 use NSThread to set thread name under on apple plafrorms
don't use pthread_setname_np
2026-08-19 19:36:05 +02:00

919 lines
30 KiB
C++

#include "utils.h"
#include <cmath> // std::round
#include "epro_thread.h"
#include "config.h"
#include "fmt.h"
#include "logging.h"
#include "deck_manager.h"
#include "replay.h"
#if EDOPRO_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h> // ShellExecute
#include "utils_gui.h"
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4091)
#endif
#include <dbghelp.h>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#endif //EDOPRO_WINDOWS
#if EDOPRO_POSIX
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pthread.h>
#include <limits.h> // PATH_MAX
using Stat = struct stat;
#include "porting.h"
#endif //EDOPRO_LINUX_KERNEL || EDOPRO_APPLE
#if EDOPRO_LINUX
#include <sys/wait.h>
#endif //EDOPRO_LINUX
#if EDOPRO_LINUX_KERNEL
#include <sys/sendfile.h>
#include <fcntl.h>
#endif //EDOPRO_LINUX_KERNEL
#if EDOPRO_APPLE
#if EDOPRO_MACOS
#include <CoreFoundation/CoreFoundation.h>
#include <mach-o/dyld.h>
#include <CoreServices/CoreServices.h>
#endif //EDOPRO_MACOS
#include <copyfile.h>
#endif //EDOPRO_APPLE
#include <IFileArchive.h>
#include <IFileSystem.h>
#include <IOSOperator.h>
#include "bufferio.h"
#include "file_stream.h"
#if EDOPRO_WINDOWS
namespace {
//https://docs.microsoft.com/en-us/visualstudio/debugger/how-to-set-a-thread-name-in-native-code?view=vs-2015&redirectedfrom=MSDN
struct THREADNAME_INFO {
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
};
constexpr DWORD MS_VC_EXCEPTION = 0x406D1388;
LONG NTAPI PvectoredExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo) {
if(ExceptionInfo->ExceptionRecord->ExceptionCode == MS_VC_EXCEPTION)
return EXCEPTION_CONTINUE_EXECUTION;
return EXCEPTION_CONTINUE_SEARCH;
}
inline void NameThreadMsvc(const char* threadName) {
const THREADNAME_INFO info{ 0x1000, threadName, static_cast<DWORD>(-1), 0 };
auto handle = AddVectoredExceptionHandler(1, PvectoredExceptionHandler);
if(handle) {
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), reinterpret_cast<const ULONG_PTR*>(&info));
RemoveVectoredExceptionHandler(handle);
}
}
const auto PSetThreadDescription = [] {
auto proc = GetProcAddress(GetModuleHandle(EPRO_TEXT("kernel32.dll")), "SetThreadDescription");
if(proc == nullptr)
proc = GetProcAddress(GetModuleHandle(EPRO_TEXT("KernelBase.dll")), "SetThreadDescription");
using SetThreadDescription_t = HRESULT(WINAPI*)(HANDLE, PCWSTR);
return function_cast<SetThreadDescription_t>(proc);
}();
void NameThread(const char* name, const wchar_t* wname) {
NameThreadMsvc(name);
if(PSetThreadDescription)
PSetThreadDescription(GetCurrentThread(), wname);
}
//Dump creation routines taken from Postgres
//https://github.com/postgres/postgres/blob/27b77ecf9f4d5be211900eda54d8155ada50d696/src/backend/port/win32/crashdump.c
LONG WINAPI crashDumpHandler(EXCEPTION_POINTERS* pExceptionInfo) {
using MiniDumpWriteDump_t = BOOL(WINAPI*) (HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
ygo::GUIUtils::ShowErrorWindow("Crash", "The program crashed, a crash dump will be created");
if(!ygo::Utils::MakeDirectory(EPRO_TEXT("./crashdumps")))
return EXCEPTION_CONTINUE_SEARCH;
/* 'crashdumps' exists and is a directory. Try to write a dump' */
HANDLE selfProcHandle = GetCurrentProcess();
DWORD selfPid = GetCurrentProcessId();
MINIDUMP_EXCEPTION_INFORMATION ExInfo{ GetCurrentThreadId(), pExceptionInfo, FALSE };
/* Load the dbghelp.dll library and functions */
auto* dbgHelpDLL = LoadLibrary(EPRO_TEXT("dbghelp.dll"));
if(dbgHelpDLL == nullptr)
return EXCEPTION_CONTINUE_SEARCH;
auto* miniDumpWriteDumpFn = function_cast<MiniDumpWriteDump_t>(GetProcAddress(dbgHelpDLL, "MiniDumpWriteDump"));
if(miniDumpWriteDumpFn == nullptr) {
FreeLibrary(dbgHelpDLL);
return EXCEPTION_CONTINUE_SEARCH;
}
/*
* Dump as much as we can, except shared memory, code segments, and
* memory mapped files. Exactly what we can dump depends on the
* version of dbghelp.dll, see:
* http://msdn.microsoft.com/en-us/library/ms680519(v=VS.85).aspx
*/
auto dumpType = static_cast<MINIDUMP_TYPE>(MiniDumpNormal | MiniDumpWithHandleData | MiniDumpWithDataSegs);
if(GetProcAddress(dbgHelpDLL, "EnumDirTree") != nullptr) {
/* If this function exists, we have version 5.2 or newer */
dumpType = static_cast<MINIDUMP_TYPE>(dumpType | MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithPrivateReadWriteMemory);
}
auto systemTicks = GetTickCount();
const auto dumpPath = epro::format(EPRO_TEXT("./crashdumps/EDOPro-pid{}-{}.mdmp"), (int)selfPid, (int)systemTicks);
auto dumpFile = CreateFile(dumpPath.data(), GENERIC_WRITE, FILE_SHARE_WRITE,
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
if(dumpFile == INVALID_HANDLE_VALUE) {
FreeLibrary(dbgHelpDLL);
return EXCEPTION_CONTINUE_SEARCH;
}
if(miniDumpWriteDumpFn(selfProcHandle, selfPid, dumpFile, dumpType, &ExInfo, nullptr, nullptr))
ygo::GUIUtils::ShowErrorWindow("Crash dump", epro::format("Succesfully wrote crash dump to file \"{}\"\n", ygo::Utils::ToUTF8IfNeeded(dumpPath)));
CloseHandle(dumpFile);
FreeLibrary(dbgHelpDLL);
return EXCEPTION_CONTINUE_SEARCH;
}
}
#endif
namespace ygo {
std::vector<SynchronizedIrrArchive> Utils::archives;
irr::io::IFileSystem* Utils::filesystem{ nullptr };
irr::ITimer* Utils::irrTimer{ nullptr };
irr::IOSOperator* Utils::OSOperator{ nullptr };
RNG::SplitMix64 Utils::generator(std::chrono::high_resolution_clock::now().time_since_epoch().count());
void Utils::InternalSetThreadName(const char* name, [[maybe_unused]] const wchar_t* wname) {
#if EDOPRO_WINDOWS
NameThread(name, wname);
#elif EDOPRO_LINUX_KERNEL
pthread_setname_np(pthread_self(), name);
#elif EDOPRO_APPLE
porting::nameThread(name);
#endif //EDOPRO_WINDOWS
}
epro::thread::id Utils::GetCurrThreadId() {
return epro::this_thread::get_id();
}
extern epro::thread::id main_thread_id;
epro::thread::id Utils::GetMainThreadId() {
return main_thread_id;
}
void Utils::SetupCrashDumpLogging() {
#if EDOPRO_WINDOWS
SetUnhandledExceptionFilter(crashDumpHandler);
#endif
}
bool Utils::IsRunningAsAdmin() {
#if EDOPRO_WINDOWS
return false;
// this code returned false positives
#if 0
// https://stackoverflow.com/questions/8046097/how-to-check-if-a-process-has-the-administrative-rights
HANDLE hToken = nullptr;
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
return false;
TOKEN_ELEVATION Elevation;
DWORD cbSize = sizeof(TOKEN_ELEVATION);
auto got_info = GetTokenInformation(hToken, TokenElevation, &Elevation, sizeof(Elevation), &cbSize);
CloseHandle(hToken);
if(got_info && Elevation.TokenIsElevated)
return true;
#endif
#elif EDOPRO_LINUX || EDOPRO_MACOS
auto uid = getuid();
auto euid = geteuid();
// if either effective uid or uid is the one of the root user assume running as root.
// else if euid and uid are different then permissions errors can happen if its running
// as a completly different user than the uid/euid
if(uid == 0 || euid == 0 || uid != euid)
return true;
#endif
return false;
}
namespace {
std::map<epro::thread::id, std::string> last_error_strings;
epro::mutex last_error_strings_mutex;
std::string& last_error_string_() {
const auto id = Utils::GetCurrThreadId();
std::unique_lock<epro::mutex> lk{ last_error_strings_mutex };
auto it = last_error_strings.find(id);
if(it != last_error_strings.end())
return it->second;
return last_error_strings[id];
}
}
epro::stringview Utils::GetLastErrorString() {
return last_error_string_();
}
static void SetLastError() {
auto& last_error_string = last_error_string_();
#if EDOPRO_WINDOWS
const auto error = GetLastError();
if(error == NOERROR) {
last_error_string.clear();
return;
}
static constexpr DWORD formatControl = FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_IGNORE_INSERTS |
FORMAT_MESSAGE_FROM_SYSTEM;
LPTSTR textBuffer = nullptr;
auto count = FormatMessage(formatControl, nullptr, error, 0, (LPTSTR)&textBuffer, 0, nullptr);
if(count != 0) {
last_error_string = Utils::ToUTF8IfNeeded(textBuffer);
LocalFree(textBuffer);
if(last_error_string.back() == '\n')
last_error_string.pop_back();
if(last_error_string.back() == '\r')
last_error_string.pop_back();
return;
}
last_error_string = "Unknown error";
#else
last_error_string = strerror(errno);
#endif
}
static inline bool SetLastErrorStringIfFailed(bool check) {
if(check)
return true;
SetLastError();
return false;
}
bool Utils::MakeDirectory(epro::path_stringview path) {
#if EDOPRO_WINDOWS
return SetLastErrorStringIfFailed(CreateDirectory(path.data(), nullptr) || ERROR_ALREADY_EXISTS == GetLastError());
#else
#if EDOPRO_ANDROID
if(porting::pathIsUri(path))
return SetLastErrorStringIfFailed(porting::createFolderUri(path));
#endif
return SetLastErrorStringIfFailed(mkdir(path.data(), 0777) == 0 || errno == EEXIST);
#endif
}
bool Utils::FileCopyFD([[maybe_unused]] int source, [[maybe_unused]] int destination) {
#if EDOPRO_LINUX_KERNEL
off_t bytesCopied = 0;
Stat fileinfo{};
fstat(source, &fileinfo);
int result = sendfile(destination, source, &bytesCopied, fileinfo.st_size);
return SetLastErrorStringIfFailed(result != -1);
#elif EDOPRO_APPLE
return SetLastErrorStringIfFailed(fcopyfile(source, destination, 0, COPYFILE_ALL) == 0);
#else
return false;
#endif
}
bool Utils::FileCopy(epro::path_stringview source, epro::path_stringview destination) {
if(source == destination)
return false;
#if EDOPRO_WINDOWS
return SetLastErrorStringIfFailed(CopyFile(source.data(), destination.data(), false));
#elif EDOPRO_LINUX_KERNEL
#if EDOPRO_ANDROID
#define NEEDS_FSTREAM
if(!porting::pathIsUri(source) && !porting::pathIsUri(destination))
#endif
{
int input, output;
if((input = open(source.data(), O_RDONLY)) == -1) {
SetLastError();
return false;
}
Stat fileinfo{};
fstat(input, &fileinfo);
if((output = creat(destination.data(), fileinfo.st_mode)) == -1) {
SetLastError();
close(input);
return false;
}
auto result = FileCopyFD(input, output);
close(input);
close(output);
return result;
}
#elif EDOPRO_APPLE
return SetLastErrorStringIfFailed(copyfile(source.data(), destination.data(), 0, COPYFILE_ALL) == 0);
#else
#define NEEDS_FSTREAM
#endif
#if defined(NEEDS_FSTREAM)
#undef NEEDS_FSTREAM
FileStream src(source.data(), FileStream::in | FileStream::binary);
if(!src.is_open())
return false;
FileStream dst(destination.data(), FileStream::out | FileStream::binary);
if(!dst.is_open())
return false;
dst << src.rdbuf();
src.close();
dst.close();
return true;
#endif
}
bool Utils::FileMove(epro::path_stringview source, epro::path_stringview destination) {
if(source == destination)
return true;
#if EDOPRO_WINDOWS
return SetLastErrorStringIfFailed(MoveFile(source.data(), destination.data()));
#else
#if EDOPRO_ANDROID
if(porting::pathIsUri(source) || porting::pathIsUri(destination)) {
if(!FileCopy(source, destination))
return false;
if(!FileDelete(source)) {
FileDelete(destination);
return false;
}
return true;
}
#endif
return SetLastErrorStringIfFailed(rename(source.data(), destination.data()) == 0);
#endif
}
bool Utils::FileExists(epro::path_stringview path) {
#if EDOPRO_WINDOWS
const auto dwAttrib = GetFileAttributes(path.data());
return (dwAttrib != INVALID_FILE_ATTRIBUTES && !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY));
#else
Stat sb;
return stat(path.data(), &sb) != -1 && S_ISREG(sb.st_mode) != 0;
#endif
}
static epro::path_string working_dir;
bool Utils::SetWorkingDirectory(epro::path_stringview newpath) {
#if EDOPRO_WINDOWS
bool res = SetLastErrorStringIfFailed(SetCurrentDirectory(newpath.data()));
#elif EDOPRO_IOS
bool res = porting::changeWorkDir(newpath.data()) == 1;
#else
bool res = SetLastErrorStringIfFailed(chdir(newpath.data()) == 0);
#endif
if(res)
working_dir = NormalizePathImpl(newpath);
return res;
}
const epro::path_string& Utils::GetWorkingDirectory() {
return working_dir;
}
static epro::path_string user_storage_dir;
void Utils::SetUserStorageDirectory(epro::path_stringview newpath) {
user_storage_dir = NormalizePath(GetAbsolutePath(newpath));
}
const epro::path_string& Utils::GetUserStorageDirectory() {
return user_storage_dir;
}
epro::path_string Utils::GetUserFolderPathFor(epro::path_stringview path) {
return NormalizePath(epro::format(EPRO_TEXT("{}/{}"), GetUserStorageDirectory(), path));
}
bool Utils::FileDelete(epro::path_stringview source) {
#if EDOPRO_WINDOWS
return SetLastErrorStringIfFailed(DeleteFile(source.data()));
#else
#if EDOPRO_ANDROID
if(porting::pathIsUri(source))
return porting::deleteFileUri(source);
#endif
return SetLastErrorStringIfFailed(remove(source.data()) == 0);
#endif
}
void Utils::FindFiles(epro::path_stringview _path, const std::function<void(epro::path_stringview, bool)>& cb) {
const auto path = Utils::NormalizePath(_path);
#if EDOPRO_WINDOWS
WIN32_FIND_DATA fdata;
auto fh = FindFirstFile(epro::format(EPRO_TEXT("{}*.*"), path).data(), &fdata);
if(fh != INVALID_HANDLE_VALUE) {
do {
cb(fdata.cFileName, !!(fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY));
} while(FindNextFile(fh, &fdata));
FindClose(fh);
}
#else
#if EDOPRO_ANDROID
if(porting::pathIsUri(path)) {
bool found_curdir = false;
bool found_topdir = false;
auto entries = porting::listFilesInFolder(path);
for(auto& entry : entries) {
bool isdir = entry.back() == EPRO_TEXT('/');
if(isdir)
entry.pop_back();
cb(entry, isdir);
if(entry == EPRO_TEXT("."sv))
found_curdir = true;
if(entry == EPRO_TEXT(".."sv))
found_topdir = true;
}
if(!found_curdir)
cb(EPRO_TEXT("."), true);
if(!found_topdir)
cb(EPRO_TEXT(".."), true);
} else
#endif
if(auto dir = opendir(path.data())) {
#if EDOPRO_ANDROID
// workaround, on android 11 (and probably higher) the folders "." and ".." aren't
// returned by readdir, assuming the parsed path will never be root, manually
// pass those 2 folders if they aren't returned by readdir
bool found_curdir = false;
bool found_topdir = false;
#endif //EDOPRO_ANDROID
while(auto dirp = readdir(dir)) {
bool isdir = false;
#ifdef _DIRENT_HAVE_D_TYPE //avoid call to format and stat
if(dirp->d_type != DT_LNK && dirp->d_type != DT_UNKNOWN) {
isdir = dirp->d_type == DT_DIR;
if(!isdir && dirp->d_type != DT_REG) //not a folder or file, skip
continue;
} else
#endif
{
Stat fileStat;
stat(epro::format("{}{}", path, dirp->d_name).data(), &fileStat);
isdir = !!S_ISDIR(fileStat.st_mode);
if(!isdir && !S_ISREG(fileStat.st_mode)) //not a folder or file, skip
continue;
}
#if EDOPRO_ANDROID
if(dirp->d_name == EPRO_TEXT("."sv))
found_curdir = true;
if(dirp->d_name == EPRO_TEXT(".."sv))
found_topdir = true;
#endif //EDOPRO_ANDROID
cb(dirp->d_name, isdir);
}
#if EDOPRO_ANDROID
if(!found_curdir)
cb(EPRO_TEXT("."), true);
if(!found_topdir)
cb(EPRO_TEXT(".."), true);
#endif //EDOPRO_ANDROID
closedir(dir);
}
#endif
}
#define ROOT_OR_CUR(str) (str == EPRO_TEXT(".") || (str == EPRO_TEXT("..")))
bool Utils::ClearDirectory(epro::path_stringview path) {
FindFiles(path, [&path](epro::path_stringview name, bool isdir) {
if(isdir) {
if(!ROOT_OR_CUR(name))
DeleteDirectory(epro::format(EPRO_TEXT("{}{}/"), path, name));
} else
FileDelete(epro::format(EPRO_TEXT("{}{}"), path, name));
});
return true;
}
bool Utils::DeleteDirectory(epro::path_stringview path) {
ClearDirectory(path);
#if EDOPRO_WINDOWS
return RemoveDirectory(path.data());
#else
#if EDOPRO_ANDROID
if(porting::pathIsUri(path))
return porting::deleteFileUri(path);
#endif
return rmdir(path.data()) == 0;
#endif
}
bool Utils::DirectoryExists(epro::path_stringview path) {
#if EDOPRO_WINDOWS
const auto dwAttrib = GetFileAttributes(path.data());
return (dwAttrib != INVALID_FILE_ATTRIBUTES && ((dwAttrib & FILE_ATTRIBUTE_DIRECTORY) != 0));
#else
Stat sb;
return stat(path.data(), &sb) != -1 && S_ISDIR(sb.st_mode) != 0;
#endif
}
void Utils::CreateResourceFolders() {
auto createResourceDirAndLogIfFailure = [](auto fn, epro::path_stringview path) {
if(!fn(path)) {
ygo::ErrorLog("Failed to create resource folder {} ({})", Utils::ToUTF8IfNeeded(path), GetLastErrorString());
}
};
//create directories if missing
createResourceDirAndLogIfFailure(MakeDirectory, DeckManager::GetDeckFolder());
createResourceDirAndLogIfFailure(MakeDirectory, EPRO_TEXT("puzzles"));
createResourceDirAndLogIfFailure(MakeDirectory, EPRO_TEXT("pics"));
createResourceDirAndLogIfFailure(MakeDirectory, EPRO_TEXT("pics/field"));
createResourceDirAndLogIfFailure(MakeDirectory, EPRO_TEXT("pics/cover"));
createResourceDirAndLogIfFailure(MakeDirectory, EPRO_TEXT("pics/temp/"));
createResourceDirAndLogIfFailure(ClearDirectory, EPRO_TEXT("pics/temp/"));
createResourceDirAndLogIfFailure(MakeDirectory, Replay::GetReplayFolder());
createResourceDirAndLogIfFailure(MakeDirectory, EPRO_TEXT("screenshots"));
}
std::vector<epro::path_string> Utils::FindFiles(epro::path_stringview path, const std::vector<epro::path_stringview>& extensions, int subdirectorylayers) {
std::vector<epro::path_string> res;
FindFiles(path, [&res, extensions, path, subdirectorylayers](epro::path_stringview name, bool isdir) {
if(isdir) {
if(subdirectorylayers && !ROOT_OR_CUR(name)) {
auto res2 = FindFiles(epro::format(EPRO_TEXT("{}{}/"), path, name), extensions, subdirectorylayers - 1);
for(auto& file : res2)
file = epro::format(EPRO_TEXT("{}/{}"), name, file);
res.insert(res.end(), std::make_move_iterator(res2.begin()), std::make_move_iterator(res2.end()));
}
} else {
if(extensions.empty() || std::find(extensions.begin(), extensions.end(), Utils::GetFileExtension(name)) != extensions.end())
res.emplace_back(name.data(), name.size());
}
});
std::sort(res.begin(), res.end(), CompareIgnoreCase<epro::path_string>);
return res;
}
std::vector<epro::path_string> Utils::FindSubfolders(epro::path_stringview path, int subdirectorylayers, bool addparentpath) {
std::vector<epro::path_string> results;
FindFiles(path, [&results, path, subdirectorylayers, addparentpath](epro::path_stringview name, bool isdir) {
if (!isdir || ROOT_OR_CUR(name))
return;
epro::path_string fullpath = epro::format(EPRO_TEXT("{}{}/"), path, name);
epro::path_stringview cur = name;
if(addparentpath)
cur = fullpath;
results.emplace_back(cur.data(), cur.size());
if(subdirectorylayers > 1) {
auto subresults = FindSubfolders(fullpath, subdirectorylayers - 1, false);
for(auto& folder : subresults) {
folder = epro::format(EPRO_TEXT("{}{}/"), fullpath, folder);
}
results.insert(results.end(), std::make_move_iterator(subresults.begin()), std::make_move_iterator(subresults.end()));
}
});
std::sort(results.begin(), results.end(), CompareIgnoreCase<epro::path_string>);
return results;
}
std::vector<uint32_t> Utils::FindFiles(irr::io::IFileArchive* archive, epro::path_stringview path, const std::vector<epro::path_stringview>& extensions, int subdirectorylayers) {
std::vector<uint32_t> res;
auto list = archive->getFileList();
for(irr::u32 i = 0; i < list->getFileCount(); i++) {
if(list->isDirectory(i))
continue;
const auto name = list->getFullFileName(i);
if(std::count(name.data(), name.data() + name.size(), EPRO_TEXT('/')) > subdirectorylayers)
continue;
if(extensions.empty() || std::find(extensions.begin(), extensions.end(), Utils::GetFileExtension(name)) != extensions.end())
res.push_back(i);
}
return res;
}
irr::io::IReadFile* Utils::FindFileInArchives(epro::path_stringview path, epro::path_stringview name) {
for(auto& archive : archives) {
auto list = archive.archive->getFileList();
int res = list->findFile(epro::format(EPRO_TEXT("{}{}"), path, name).data());
if(res != -1) {
std::lock_guard<epro::mutex> lk(*archive.mutex);
auto reader = archive.archive->createAndOpenFile(res);
return reader;
}
}
return nullptr;
}
const std::string& Utils::GetUserAgent() {
auto EscapeUTF8 = [](auto& to_escape) {
auto IsNonANSI = [](char c) {
return (static_cast<unsigned>(c) & ~0x7Fu) != 0;
};
const epro::stringview view{ to_escape.data(), to_escape.size() };
const auto total_unicode = std::count_if(view.begin(), view.end(), IsNonANSI);
std::string ret;
ret.reserve(view.size() + (total_unicode * 4));
for(auto c : view) {
if(IsNonANSI(c)) {
static constexpr std::array<char, 16> map{ {'0', '1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'} };
auto int_char = static_cast<unsigned>(c);
ret.append(R"(\x)").append(1, map[(int_char >> 4) & 0xf]).append(1, map[int_char & 0xf]);
} else
ret.append(1, c);
}
return ret;
};
static const std::string agent = epro::format("EDOPro-" OSSTRING "-" STR(EDOPRO_VERSION_MAJOR) "." STR(EDOPRO_VERSION_MINOR) "." STR(EDOPRO_VERSION_PATCH)" {}",
EscapeUTF8(Utils::OSOperator->getOperatingSystemVersion()));
return agent;
}
epro::path_string Utils::GetAbsolutePath(epro::path_stringview path) {
#if EDOPRO_WINDOWS
auto len = GetFullPathName(path.data(), 0, nullptr, nullptr);
epro::path_string ret;
ret.resize(len);
len = GetFullPathName(path.data(), ret.size(), ret.data(), nullptr);
ret.resize(len);
std::replace(ret.begin(), ret.end(), EPRO_TEXT('\\'), EPRO_TEXT('/'));
return ret;
#else
#if EDOPRO_ANDROID
if(porting::pathIsUri(path))
return epro::path_string{ path };
#endif
char buff[PATH_MAX];
if(realpath(path.data(), buff) == nullptr)
return { path.data(), path.size() };
return buff;
#endif
}
bool Utils::ContainsSubstring(epro::wstringview input, const std::vector<epro::wstringview>& tokens) {
if (input.empty() || tokens.empty())
return false;
std::size_t pos1, pos2 = 0;
for (const auto& token : tokens) {
if((pos1 = input.find(token, pos2)) == epro::wstringview::npos)
return false;
pos2 = pos1 + token.size();
}
return true;
}
bool Utils::CreatePath(epro::path_stringview path, epro::path_string workingdir) {
const bool wasempty = workingdir.empty();
epro::path_stringview::size_type pos1, pos2 = 0;
while((pos1 = path.find(EPRO_TEXT('/'), pos2)) != epro::path_stringview::npos) {
if(pos1 != pos2) {
if(pos2 != 0 || !wasempty)
workingdir.append(1, EPRO_TEXT('/'));
workingdir.append(path.begin() + pos2, path.begin() + pos1);
if(!MakeDirectory(workingdir))
return false;
}
pos2 = pos1 + 1;
}
return true;
}
static const epro::path_string exe_path = []()->epro::path_string {
#if EDOPRO_WINDOWS
epro::path_string exepath;
exepath.resize(32768);
auto len = GetModuleFileName(nullptr, exepath.data(), exepath.size());
exepath.resize(len);
return Utils::NormalizePath(exepath, false);
#elif EDOPRO_LINUX
epro::path_char buff[PATH_MAX];
ssize_t len = ::readlink("/proc/self/exe", buff, sizeof(buff) - 1);
if(len != -1)
buff[len] = EPRO_TEXT('\0');
return buff;
#elif EDOPRO_MACOS
CFURLRef bundle_url = CFBundleCopyBundleURL(CFBundleGetMainBundle());
CFStringRef bundle_path = CFURLCopyFileSystemPath(bundle_url, kCFURLPOSIXPathStyle);
CFURLRef bundle_base_url = CFURLCreateCopyDeletingLastPathComponent(nullptr, bundle_url);
CFRelease(bundle_url);
CFStringRef path = CFURLCopyFileSystemPath(bundle_base_url, kCFURLPOSIXPathStyle);
CFRelease(bundle_base_url);
/*
#ifdef MAC_OS_DISCORD_LAUNCHER
//launches discord launcher so that it's registered as bundle to launch by discord
system(epro::format("open {}/Contents/MacOS/discord-launcher.app --args random", CFStringGetCStringPtr(bundle_path, kCFStringEncodingUTF8)).data());
#endif
*/
epro::path_string res = epro::path_string(CFStringGetCStringPtr(path, kCFStringEncodingUTF8)) + "/";
CFRelease(path);
CFRelease(bundle_path);
return res;
#else
return EPRO_TEXT("");
#endif
}();
const epro::path_string& Utils::GetExePath() {
return exe_path;
}
static const epro::path_string exe_folder = Utils::GetFilePath(exe_path);
const epro::path_string& Utils::GetExeFolder() {
return exe_folder;
}
static const epro::path_string core_path = [] {
#if EDOPRO_WINDOWS
return epro::format(EPRO_TEXT("{}/ocgcore.dll"), Utils::GetExeFolder());
#else
return EPRO_TEXT(""); // Unused on POSIX
#endif
}();
const epro::path_string& Utils::GetCorePath() {
return core_path;
}
bool Utils::UnzipArchive(epro::path_stringview input, unzip_callback callback, unzip_payload* payload, epro::path_stringview dest) {
std::vector<char> buff;
buff.resize(0x2000);
if(!filesystem)
return false;
CreatePath(dest, EPRO_TEXT("./"));
irr::io::IFileArchive* archive = nullptr;
if(!filesystem->addFileArchive({ input.data(), static_cast<irr::u32>(input.size()) }, false, false, irr::io::EFAT_ZIP, "", &archive))
return false;
archive->grab();
auto filelist = archive->getFileList();
auto count = filelist->getFileCount();
irr::u32 totsize = 0;
irr::u32 cur_fullsize = 0;
for(irr::u32 i = 0; i < count; i++)
totsize += filelist->getFileSize(i);
if(payload)
payload->tot = count;
for(irr::u32 i = 0; i < count; i++) {
auto filename = filelist->getFullFileName(i);
bool isdir = filelist->isDirectory(i);
if(isdir)
CreatePath(epro::format(EPRO_TEXT("{}/"), filename), { dest.data(), dest.size() });
else
CreatePath({ filename.data(), filename.size() }, { dest.data(), dest.size() });
if(!isdir) {
int percentage = 0;
auto reader = archive->createAndOpenFile(i);
if(reader) {
if(payload) {
payload->is_new = true;
payload->cur = i;
payload->percentage = 0;
payload->filename = filename.data();
callback(payload);
payload->is_new = false;
}
FileStream out{ epro::format(EPRO_TEXT("{}/{}"), dest, filename), FileStream::out | FileStream::binary };
size_t r, rx = reader->getSize();
for(r = 0; r < rx; /**/) {
int wx = static_cast<int>(reader->read(buff.data(), buff.size()));
out.write(buff.data(), wx);
r += wx;
cur_fullsize += wx;
if(callback && totsize) {
double fractiondownloaded = (double)cur_fullsize / (double)rx;
percentage = (int)std::round(fractiondownloaded * 100);
if(payload)
payload->percentage = percentage;
callback(payload);
}
}
reader->drop();
}
}
}
filesystem->removeFileArchive(archive);
archive->drop();
return true;
}
void Utils::SystemOpen(epro::path_stringview arg, OpenType type) {
#if EDOPRO_WINDOWS
if(type == SHARE_FILE)
return;
ShellExecute(nullptr, EPRO_TEXT("open"), (type == OPEN_FILE) ? epro::format(EPRO_TEXT("{}/{}"), GetWorkingDirectory(), arg).data() : arg.data(), nullptr, nullptr, SW_SHOWNORMAL);
#elif EDOPRO_MACOS || EDOPRO_LINUX
if(type == SHARE_FILE)
return;
#if EDOPRO_MACOS
#define OPEN "open"
#else
#define OPEN "xdg-open"
#endif
const auto* arg_cstr = arg.data();
auto pid = vfork();
if(pid == 0) {
execl("/usr/bin/" OPEN, OPEN, arg_cstr, nullptr);
_exit(EXIT_FAILURE);
} else if(pid < 0)
perror("Failed to fork:");
if(waitpid(pid, nullptr, WNOHANG) != 0)
perror("Failed to open arg or file:");
#elif EDOPRO_ANDROID
switch(type) {
case OPEN_FILE:
if(porting::pathIsUri(arg))
return porting::openFile(arg);
return porting::openFile(epro::format("{}/{}", GetWorkingDirectory(), arg));
case OPEN_URL:
return porting::openUrl(arg);
case SHARE_FILE:
if(porting::pathIsUri(arg))
return porting::shareFile(arg);
return porting::shareFile(epro::format("{}/{}", GetWorkingDirectory(), arg));
}
#endif
}
void Utils::Reboot() {
#if EDOPRO_WINDOWS || EDOPRO_LINUX || EDOPRO_MACOS
const auto& path = GetExePath();
#if EDOPRO_WINDOWS
STARTUPINFO si{ sizeof(si) };
PROCESS_INFORMATION pi{};
auto command = epro::format(EPRO_TEXT("{} -C \"{}\" -l"), GetFileName(path, true), GetWorkingDirectory());
if(!CreateProcess(path.data(), &command[0], nullptr, nullptr, false, 0, nullptr, nullptr, &si, &pi))
return;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
#else
#if EDOPRO_LINUX
struct stat fileStat;
stat(path.data(), &fileStat);
chmod(path.data(), fileStat.st_mode | S_IXUSR | S_IXGRP | S_IXOTH);
#endif
{
[[maybe_unused]] const auto* path_cstr = path.data();
const auto& workdir = GetWorkingDirectory();
const auto* workdir_cstr = workdir.data();
auto pid = vfork();
if(pid == 0) {
#if EDOPRO_LINUX
execl(path_cstr, path_cstr, "-C", workdir_cstr, "-l", nullptr);
#else
execlp("open", "open", "-b", "io.github.edo9300.ygoprodll", "--args", "-C", workdir_cstr, "-l", nullptr);
#endif
_exit(EXIT_FAILURE);
}
if(pid < 0 || waitpid(pid, nullptr, WNOHANG) != 0)
return;
}
#endif
exit(0);
#endif
}
std::wstring Utils::ReadPuzzleMessage(epro::wstringview script_name) {
FileStream infile{ Utils::ToPathString(script_name), FileStream::in };
if(infile.fail())
return {};
std::string str;
std::string res = "";
size_t start = std::string::npos;
bool stop = false;
while(!stop && std::getline(infile, str)) {
auto pos = str.find('\r');
if(str.size() && pos != std::string::npos)
str.erase(pos);
bool was_empty = str.empty();
if(start == std::string::npos) {
start = str.find("--[[message");
if(start == std::string::npos)
continue;
str.erase(0, start + 11);
}
size_t end = str.find("]]");
if(end != std::string::npos) {
str.erase(end);
stop = true;
}
if(str.empty() && !was_empty)
continue;
if(!res.empty() || was_empty)
res += "\n";
res += str;
}
return BufferIO::DecodeUTF8(res);
}
}