/* Copyright 2021 flyinghead This file is part of Flycast. Flycast 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, either version 2 of the License, or (at your option) any later version. Flycast 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 for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "oslib.h" #include "stdclass.h" #include "cfg/cfg.h" #include "cfg/option.h" #include "nowide/fstream.hpp" #include "storage.h" #ifndef _WIN32 #include #endif #if defined(USE_SDL) #include "sdl/sdl.h" #else #if defined(SUPPORT_X11) #include "linux-dist/x11.h" #endif #if defined(USE_EVDEV) #include "linux-dist/evdev.h" #endif #endif #if defined(_WIN32) && !defined(TARGET_UWP) #include "windows/rawinput.h" #include #endif #include "profiler/fc_profiler.h" namespace hostfs { std::string getVmuPath(const std::string& port) { if (port == "A1" && config::PerGameVmu && !settings.content.path.empty()) return get_game_save_prefix() + "_vmu_save_A1.bin"; char tempy[512]; sprintf(tempy, "vmu_save_%s.bin", port.c_str()); // VMU saves used to be stored in .reicast, not in .reicast/data std::string apath = get_writable_config_path(tempy); if (!file_exists(apath)) apath = get_writable_data_path(tempy); return apath; } std::string getArcadeFlashPath() { std::string nvmemSuffix = cfgLoadStr("net", "nvmem", ""); return get_game_save_prefix() + nvmemSuffix; } std::string findFlash(const std::string& prefix, const std::string& names) { const size_t npos = std::string::npos; size_t start = 0; while (start < names.size()) { size_t semicolon = names.find(';', start); std::string name = names.substr(start, semicolon == npos ? semicolon : semicolon - start); size_t percent = name.find('%'); if (percent != npos) name = name.replace(percent, 1, prefix); std::string fullpath = get_readonly_data_path(name); if (hostfs::storage().exists(fullpath)) return fullpath; for (const auto& path : config::ContentPath.get()) { try { fullpath = hostfs::storage().getSubPath(path, name); if (hostfs::storage().exists(fullpath)) return fullpath; } catch (const hostfs::StorageException& e) { } } start = semicolon; if (start != npos) start++; } return ""; } std::string getFlashSavePath(const std::string& prefix, const std::string& name) { return get_writable_data_path(prefix + name); } std::string findNaomiBios(const std::string& name) { std::string fullpath = get_readonly_data_path(name); if (hostfs::storage().exists(fullpath)) return fullpath; for (const auto& path : config::ContentPath.get()) { try { fullpath = hostfs::storage().getSubPath(path, name); if (hostfs::storage().exists(fullpath)) return fullpath; } catch (const hostfs::StorageException& e) { } } return ""; } std::string getSavestatePath(int index, bool writable) { std::string state_file = get_file_basename(settings.content.fileName); char index_str[4] = ""; if (index > 0) // When index is 0, use same name before multiple states is added sprintf(index_str, "_%d", std::min(99, index)); state_file = state_file + index_str + ".state"; if (index == -1) state_file += ".net"; static std::string lastFile; static std::string lastPath; if (writable) { lastFile.clear(); return get_writable_data_path(state_file); } else { if (lastFile != state_file) { lastFile = state_file; lastPath = get_readonly_data_path(state_file); } return lastPath; } } std::string getShaderCachePath(const std::string& filename) { return get_writable_data_path(filename); } std::string getTextureLoadPath(const std::string& gameId) { if (gameId.length() > 0) return get_readonly_data_path("textures/" + gameId) + "/"; else return ""; } std::string getTextureDumpPath() { return get_writable_data_path("texdump/"); } #if defined(__unix__) && !defined(__ANDROID__) static std::string runCommand(const std::string& cmd) { char buf[1024] {}; FILE *fp = popen(cmd.c_str(), "r"); if (fp == nullptr) { INFO_LOG(COMMON, "popen failed: %d", errno); return ""; } std::string result; while (fgets(buf, sizeof(buf), fp) != nullptr) result += trim_trailing_ws(buf, "\n"); int rc; if ((rc = pclose(fp)) != 0) { INFO_LOG(COMMON, "Command error: %d", rc); return ""; } return result; } static std::string getScreenshotsPath() { std::string picturesPath = runCommand("xdg-user-dir PICTURES"); if (!picturesPath.empty()) return picturesPath; const char *home = nowide::getenv("HOME"); if (home != nullptr) return home; else return "."; } #elif defined(TARGET_UWP) //TODO move to shell/uwp? using namespace Platform; using namespace Windows::Foundation; using namespace Windows::Storage; void saveScreenshot(const std::string& name, const std::vector& data) { try { StorageFolder^ folder = KnownFolders::PicturesLibrary; // or SavedPictures? if (folder == nullptr) { INFO_LOG(COMMON, "KnownFolders::PicturesLibrary is null"); throw FlycastException("Can't find Pictures library"); } nowide::wstackstring wstr; wchar_t *wname = wstr.convert(name.c_str()); String^ msname = ref new String(wname); ArrayReference arrayRef(const_cast(&data[0]), data.size()); IAsyncOperation^ op = folder->CreateFileAsync(msname, CreationCollisionOption::FailIfExists); cResetEvent asyncEvent; op->Completed = ref new AsyncOperationCompletedHandler( [&asyncEvent, &arrayRef](IAsyncOperation^ op, AsyncStatus) { IAsyncAction^ action = FileIO::WriteBytesAsync(op->GetResults(), arrayRef); action->Completed = ref new AsyncActionCompletedHandler( [&asyncEvent](IAsyncAction^, AsyncStatus){ asyncEvent.Set(); }); }); asyncEvent.Wait(); } catch (COMException^ e) { WARN_LOG(COMMON, "Save screenshot failed: %S", e->Message->Data()); throw FlycastException(""); } } #elif defined(_WIN32) && !defined(TARGET_UWP) static std::string getScreenshotsPath() { wchar_t *screenshotPath; if (FAILED(SHGetKnownFolderPath(FOLDERID_Screenshots, KF_FLAG_DEFAULT, NULL, &screenshotPath))) return get_writable_config_path(""); nowide::stackstring path; std::string ret; if (path.convert(screenshotPath) == nullptr) ret = get_writable_config_path(""); else ret = path.get(); CoTaskMemFree(screenshotPath); return ret; } #else std::string getScreenshotsPath(); #endif #if !defined(__ANDROID__) && !defined(TARGET_UWP) && !defined(TARGET_IPHONE) && !defined(__SWITCH__) void saveScreenshot(const std::string& name, const std::vector& data) { std::string path = getScreenshotsPath(); path += "/" + name; FILE *f = nowide::fopen(path.c_str(), "wb"); if (f == nullptr) throw FlycastException(path); if (std::fwrite(&data[0], data.size(), 1, f) != 1) { std::fclose(f); unlink(path.c_str()); throw FlycastException(path); } std::fclose(f); } #endif } // namespace hostfs void os_CreateWindow() { #if defined(USE_SDL) sdl_window_create(); #elif defined(SUPPORT_X11) x11_window_create(); #endif } void os_DestroyWindow() { #if defined(USE_SDL) sdl_window_destroy(); #elif defined(SUPPORT_X11) x11_window_destroy(); #endif } void os_SetupInput() { #if defined(USE_SDL) input_sdl_init(); #else #if defined(SUPPORT_X11) input_x11_init(); #endif #if defined(USE_EVDEV) input_evdev_init(); #endif #endif #if defined(_WIN32) && !defined(TARGET_UWP) if (config::UseRawInput) rawinput::init(); #endif } void os_TermInput() { #if defined(USE_SDL) input_sdl_quit(); #else #if defined(USE_EVDEV) input_evdev_close(); #endif #endif #if defined(_WIN32) && !defined(TARGET_UWP) if (config::UseRawInput) rawinput::term(); #endif } void os_UpdateInputState() { FC_PROFILE_SCOPE; #if defined(USE_SDL) input_sdl_handle(); #else #if defined(USE_EVDEV) input_evdev_handle(); #endif #endif } #ifdef USE_BREAKPAD #include "http_client.h" #include "version.h" #include "log/InMemoryListener.h" #include "wsi/context.h" #define FLYCAST_CRASH_LIST "flycast-crashes.txt" void registerCrash(const char *directory, const char *path) { char list[256]; // Register .dmp in crash list snprintf(list, sizeof(list), "%s/%s", directory, FLYCAST_CRASH_LIST); FILE *f = nowide::fopen(list, "at"); if (f != nullptr) { fprintf(f, "%s\n", path); fclose(f); } // Save last log lines InMemoryListener *listener = InMemoryListener::getInstance(); if (listener != nullptr) { strncpy(list, path, sizeof(list) - 1); list[sizeof(list) - 1] = '\0'; char *p = strrchr(list, '.'); if (p != nullptr && (p - list) < (int)sizeof(list) - 4) { strcpy(p + 1, "log"); FILE *f = nowide::fopen(list, "wt"); if (f != nullptr) { std::vector log = listener->getLog(); for (const auto& line : log) fprintf(f, "%s", line.c_str()); fprintf(f, "Version: %s\n", GIT_VERSION); fprintf(f, "Renderer: %d\n", (int)config::RendererType.get()); GraphicsContext *gctx = GraphicsContext::Instance(); if (gctx != nullptr) fprintf(f, "GPU: %s %s\n", gctx->getDriverName().c_str(), gctx->getDriverVersion().c_str()); fprintf(f, "Game: %s\n", settings.content.gameId.c_str()); fclose(f); } } } } void uploadCrashes(const std::string& directory) { FILE *f = nowide::fopen((directory + "/" FLYCAST_CRASH_LIST).c_str(), "rt"); if (f == nullptr) return; http::init(); char line[256]; bool uploadFailure = false; while (fgets(line, sizeof(line), f) != nullptr) { char *p = line + strlen(line) - 1; if (*p == '\n') *p = '\0'; if (file_exists(line)) { std::string dmpfile(line); std::string logfile = get_file_basename(dmpfile) + ".log"; #ifdef SENTRY_UPLOAD if (config::UploadCrashLogs) { NOTICE_LOG(COMMON, "Uploading minidump %s", line); std::string version = std::string(GIT_VERSION); if (file_exists(logfile)) { nowide::ifstream ifs(logfile); if (ifs.is_open()) { std::string line; while (std::getline(ifs, line)) if (line.substr(0, 9) == "Version: ") { version = line.substr(9); break; } } } std::vector fields; fields.emplace_back("upload_file_minidump", dmpfile, "application/octet-stream"); fields.emplace_back("sentry[release]", version); if (file_exists(logfile)) fields.emplace_back("flycast_log", logfile, "text/plain"); // TODO config, gpu/driver, ... int rc = http::post(SENTRY_UPLOAD, fields); if (rc >= 200 && rc < 300) { nowide::remove(dmpfile.c_str()); nowide::remove(logfile.c_str()); } else { WARN_LOG(COMMON, "Upload failed: HTTP error %d", rc); uploadFailure = true; } } else #endif { nowide::remove(dmpfile.c_str()); nowide::remove(logfile.c_str()); } } } http::term(); fclose(f); if (!uploadFailure) nowide::remove((directory + "/" FLYCAST_CRASH_LIST).c_str()); } #else void registerCrash(const char *directory, const char *path) {} void uploadCrashes(const std::string& directory) {} #endif