Skip to content

Commit 833cad4

Browse files
committed
Fix startup
1 parent 4110294 commit 833cad4

35 files changed

Lines changed: 1158 additions & 202 deletions

src/app/App.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ extern void elog(const char* msg);
77
#include "ui/MainLayout.h"
88
#include "util/Logger.h"
99
#include <cstdarg>
10+
#include <cerrno>
1011
#include <sys/stat.h>
1112
#include "net/DownloadQueue.h"
1213
#include <sysapp/launch.h>
1314
#include "util/ImageCache.h"
15+
#include "util/TextCache.h"
1416
#include "app/CacheManager.h"
1517

1618
#include <SDL2/SDL.h>
@@ -20,13 +22,11 @@ extern void elog(const char* msg);
2022
#include <SDL2/SDL_image.h>
2123

2224
#ifdef __WUT__
23-
#if BUILD_HW
2425
#include <nn/ac.h>
2526
extern "C" {
2627
void socket_lib_init();
2728
}
2829
#endif
29-
#endif
3030

3131
App::App() = default;
3232

@@ -37,6 +37,8 @@ App::~App() {
3737
m_screens.clear();
3838
elog("~App: ImageCache clear");
3939
if (m_renderer) ImageCache::get().clear(m_renderer);
40+
elog("~App: TextCache clear");
41+
TextCache::get().clear();
4042
elog("~App: DestroyRenderer");
4143
if (m_renderer) SDL_DestroyRenderer(m_renderer);
4244
elog("~App: DestroyWindow");
@@ -53,12 +55,14 @@ App::~App() {
5355
bool App::init() {
5456
elog("Network init...");
5557
#ifdef __WUT__
56-
#if BUILD_HW
58+
// Initialize the WUT network stack on both hardware AND Cemu builds.
59+
// Without these calls, Cemu's emulated socket layer never finishes
60+
// setup and curl connect() hangs. The previous BUILD_HW gate broke
61+
// cemu mode entirely (app stuck on "Loading repos...").
5762
nn::ac::Initialize();
5863
nn::ac::Connect();
5964
socket_lib_init();
6065
elog("Network ready");
61-
#endif
6266
#endif
6367

6468
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) {
@@ -97,7 +101,9 @@ bool App::init() {
97101
elog("Renderer OK");
98102

99103
elog("before logger");
100-
mkdir((Paths::modstoreBase()).c_str(), 0755);
104+
if (mkdir((Paths::modstoreBase()).c_str(), 0755) != 0 && errno != EEXIST) {
105+
elog("mkdir for app dir failed - logger may not work");
106+
}
101107
elog("mkdir done");
102108
Logger::get().init(Paths::modstoreBase() + "/app.log");
103109
elog("logger init done");
@@ -115,13 +121,17 @@ bool App::init() {
115121

116122
void App::run() {
117123
m_running = true;
124+
int pruneCounter = 0;
118125
while (m_running && !m_screens.empty() && WHBProcIsRunning()) {
119126
SDL_Event event;
120127
while (SDL_PollEvent(&event)) {
121128
if (event.type == SDL_QUIT) m_running = false;
122129
}
123130
update();
124131
render();
132+
TextCache::get().tick();
133+
// Prune unused text textures every ~5 seconds (at 60fps).
134+
if (++pruneCounter > 300) { TextCache::get().prune(); pruneCounter = 0; }
125135
}
126136
#ifdef __WUT__
127137
// Drain ProcUI until Aroma confirms exit - required for WHBProcShutdown to work

src/app/CacheManager.cpp

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "CacheManager.h"
22
#include "app/Paths.h"
33
#include "util/Logger.h"
4+
#include "net/DownloadManager.h"
45

56
#include <dirent.h>
67
#include <sys/stat.h>
@@ -65,25 +66,12 @@ void CacheManager::cleanupCorruptMods() {
6566
struct stat st;
6667
if (stat(modinfoPath.c_str(), &st) == 0) continue; // ok
6768

68-
// No modinfo.json - corrupt/partial install, remove it
69+
// No modinfo.json - corrupt/partial install, remove it.
70+
// Use the proper recursive rmrf -- the inline single-level
71+
// delete here used to leave nested subdirectories behind.
6972
LOG_WARN("CacheManager: corrupt mod folder (no modinfo.json): %s/%s - removing",
7073
titleId.c_str(), modId.c_str());
71-
72-
// rmrf the mod folder
73-
// Simple recursive delete
74-
std::string cmd = modPath; // we'll do it manually
75-
DIR* d2 = opendir(modPath.c_str());
76-
if (d2) {
77-
struct dirent* f;
78-
while ((f = readdir(d2)) != nullptr) {
79-
std::string fn = f->d_name;
80-
if (fn == "." || fn == "..") continue;
81-
std::string fp = modPath + "/" + fn;
82-
remove(fp.c_str());
83-
}
84-
closedir(d2);
85-
}
86-
if (rmdir(modPath.c_str()) == 0) removed++;
74+
if (DownloadManager::rmrf(modPath)) removed++;
8775
}
8876
closedir(modDir);
8977
}

src/app/Config.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <json.hpp>
55
#include <fstream>
66
#include <sys/stat.h>
7+
#include <cerrno>
8+
#include <cstring>
79

810
bool Config::loadFrom(const std::string& path) {
911
LOG_INFO("Loading config from: %s", path.c_str());
@@ -49,6 +51,8 @@ bool Config::load() {
4951

5052
bool Config::save() {
5153
std::string base = Paths::modstoreBase();
52-
mkdir(base.c_str(), 0755);
54+
if (mkdir(base.c_str(), 0755) != 0 && errno != EEXIST) {
55+
LOG_WARN("Config: mkdir(%s) failed: %s", base.c_str(), strerror(errno));
56+
}
5357
return saveTo(configPath());
5458
}

src/audio/AudioManager.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ void AudioManager::shutdown() {
4343
stopMusic();
4444
for (auto& [id, chunk] : m_sounds) Mix_FreeChunk(chunk);
4545
m_sounds.clear();
46-
if (m_music) { Mix_FreeMusic(m_music); m_music = nullptr; }
46+
{
47+
std::lock_guard<std::mutex> lock(m_musicMutex);
48+
if (m_music) { Mix_FreeMusic(m_music); m_music = nullptr; }
49+
}
4750
Mix_CloseAudio();
4851
Mix_Quit();
4952
m_initialized = false;
@@ -58,6 +61,7 @@ void AudioManager::playSound(SoundId id) {
5861
void AudioManager::playMusic(MusicTrack track) {
5962
if (!m_initialized || !m_musicEnabled) return;
6063
const char* file = (track == MusicTrack::Main) ? "theme_main.ogg" : "theme_alt.ogg";
64+
std::lock_guard<std::mutex> lock(m_musicMutex);
6165
if (Mix_PlayingMusic() && m_currentTrack == track) return;
6266
if (m_music) { Mix_FreeMusic(m_music); m_music = nullptr; }
6367
m_music = Mix_LoadMUS(sndPath(file).c_str());
@@ -67,9 +71,23 @@ void AudioManager::playMusic(MusicTrack track) {
6771
m_currentTrack = track;
6872
}
6973

74+
void AudioManager::playMusicFadeIn(MusicTrack track, int fadeMs) {
75+
if (!m_initialized || !m_musicEnabled) return;
76+
const char* file = (track == MusicTrack::Main) ? "theme_main.ogg" : "theme_alt.ogg";
77+
std::lock_guard<std::mutex> lock(m_musicMutex);
78+
if (Mix_PlayingMusic() && m_currentTrack == track) return;
79+
if (m_music) { Mix_FreeMusic(m_music); m_music = nullptr; }
80+
m_music = Mix_LoadMUS(sndPath(file).c_str());
81+
if (!m_music) { LOG_WARN("AudioManager: failed to load %s: %s", file, Mix_GetError()); return; }
82+
Mix_VolumeMusic(64);
83+
Mix_FadeInMusic(m_music, -1, fadeMs);
84+
m_currentTrack = track;
85+
}
86+
7087
void AudioManager::stopMusic() {
7188
if (!m_initialized) return;
7289
Mix_HaltMusic();
90+
std::lock_guard<std::mutex> lock(m_musicMutex);
7391
if (m_music) { Mix_FreeMusic(m_music); m_music = nullptr; }
7492
}
7593

src/audio/AudioManager.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <string>
33
#include <unordered_map>
4+
#include <mutex>
45
#include <SDL2/SDL_mixer.h>
56

67
enum class SoundId {
@@ -23,6 +24,7 @@ class AudioManager {
2324
void shutdown();
2425
void playSound(SoundId id);
2526
void playMusic(MusicTrack track);
27+
void playMusicFadeIn(MusicTrack track, int fadeMs);
2628
void stopMusic();
2729
bool musicEnabled() const { return m_musicEnabled; }
2830
bool soundEnabled() const { return m_soundEnabled; }
@@ -36,4 +38,5 @@ class AudioManager {
3638
MusicTrack m_currentTrack = MusicTrack::Main;
3739
std::unordered_map<int, Mix_Chunk*> m_sounds;
3840
Mix_Music* m_music = nullptr;
41+
std::mutex m_musicMutex; // protects m_music + m_currentTrack
3942
};

src/main.cpp

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,31 @@ extern "C" {
2020
}
2121
#endif
2222

23-
static FILE* g_elog = nullptr;
23+
static const char* g_elog_path = nullptr;
2424
void elog(const char* msg) {
25-
if (g_elog) {
26-
fprintf(g_elog, "%s\n", msg);
27-
fflush(g_elog);
28-
fsync(fileno(g_elog));
25+
// Always send via WHBLog so udplogserver picks it up.
26+
WHBLogPrintf("[elog] %s", msg);
27+
// On Cemu, FILE buffers don't reliably reach the host before kill, even
28+
// with fflush+fsync. Reopen+append+close per call is slower but durable.
29+
if (g_elog_path) {
30+
FILE* f = fopen(g_elog_path, "a");
31+
if (f) {
32+
fprintf(f, "%s\n", msg);
33+
fclose(f);
34+
}
2935
}
3036
}
3137

3238
int main(int argc, char** argv) {
39+
// Diagnostic: prove main() was called before ANY WHB/SDL init.
40+
{
41+
FILE* probe = fopen("/vol/external01/wiiu/apps/coffeeshop/main_called.txt", "w");
42+
if (probe) {
43+
fprintf(probe, "main reached\n");
44+
fclose(probe);
45+
}
46+
}
47+
3348
WHBProcInit();
3449
WHBLogUdpInit();
3550

@@ -43,7 +58,21 @@ int main(int argc, char** argv) {
4358
Paths::sdMounted = false;
4459
#endif
4560

46-
g_elog = fopen("fs:/vol/external01/wiiu/apps/coffeeshop/early.log", "w");
61+
// On hardware the SD mounts at "fs:/vol/external01/..." (Aroma WHB mount).
62+
// In Cemu the prefix is invalid -- pick whichever opens.
63+
{
64+
FILE* probe = fopen("fs:/vol/external01/wiiu/apps/coffeeshop/early.log", "w");
65+
if (probe) {
66+
fclose(probe);
67+
g_elog_path = "fs:/vol/external01/wiiu/apps/coffeeshop/early.log";
68+
} else {
69+
probe = fopen("/vol/external01/wiiu/apps/coffeeshop/early.log", "w");
70+
if (probe) {
71+
fclose(probe);
72+
g_elog_path = "/vol/external01/wiiu/apps/coffeeshop/early.log";
73+
}
74+
}
75+
}
4776
elog("START");
4877
elog(Paths::sdMounted ? "SD mounted" : "SD failed");
4978

@@ -76,8 +105,8 @@ int main(int argc, char** argv) {
76105
elog("ac::Finalize");
77106
nn::ac::Finalize();
78107
#endif
79-
elog("WHBProcShutdown");
80-
if (g_elog) { fflush(g_elog); fclose(g_elog); g_elog = nullptr; }
81-
WHBProcShutdown();
108+
// elog uses open+write+close per call; nothing to fclose at exit.
109+
// WHBProcShutdown intentionally omitted (CLAUDE.md): App::run drains
110+
// ProcUI and SYSLaunchMenu hands control back to the OS.
82111
return 0;
83112
}

src/mods/InstalledScanner.cpp

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,66 @@ static bool isDir(const std::string& path) {
1313
return stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode);
1414
}
1515

16+
// Iterate "/"-delimited boundaries instead of running mkdir at every char index.
17+
// Saves O(path-length) syscalls on slow SD-card filesystems.
1618
static void mkdirp(const std::string& path) {
17-
for (size_t i = 1; i <= path.size(); i++) {
18-
if (i == path.size() || path[i] == '/')
19-
mkdir(path.substr(0, i).c_str(), 0755);
19+
size_t pos = 1;
20+
while (pos < path.size()) {
21+
size_t slash = path.find('/', pos);
22+
if (slash == std::string::npos) break;
23+
mkdir(path.substr(0, slash).c_str(), 0755);
24+
pos = slash + 1;
2025
}
26+
mkdir(path.c_str(), 0755);
2127
}
2228

23-
static bool rmrf(const std::string& path) {
29+
// Recursive delete with symlink-loop protection.
30+
// - Uses lstat (not stat) so symlinks are detected before recursion.
31+
// - Tracks visited (dev,inode) pairs so a directory hardlinked or
32+
// symlinked-into-itself can't trap us in infinite recursion.
33+
// - Caps recursion depth at 64 -- Wii U mods don't go that deep, anything
34+
// beyond is malicious or corrupt.
35+
#include <set>
36+
struct VisitedKey { dev_t dev; ino_t ino; };
37+
static bool operator<(const VisitedKey& a, const VisitedKey& b) {
38+
if (a.dev != b.dev) return a.dev < b.dev;
39+
return a.ino < b.ino;
40+
}
41+
42+
static bool rmrfImpl(const std::string& path, std::set<VisitedKey>& visited, int depth) {
43+
if (depth > 64) {
44+
LOG_WARN("rmrf: depth limit hit at %s -- aborting", path.c_str());
45+
return false;
46+
}
47+
struct stat st;
48+
if (lstat(path.c_str(), &st) != 0) return true; // already gone
49+
if (!S_ISDIR(st.st_mode)) {
50+
// Symlink or regular file -- remove without following.
51+
return remove(path.c_str()) == 0;
52+
}
53+
VisitedKey key{st.st_dev, st.st_ino};
54+
if (!visited.insert(key).second) {
55+
LOG_WARN("rmrf: cycle detected at %s -- aborting", path.c_str());
56+
return false;
57+
}
58+
2459
DIR* d = opendir(path.c_str());
25-
if (!d) { remove(path.c_str()); return true; }
60+
if (!d) return false;
61+
bool ok = true;
2662
struct dirent* e;
2763
while ((e = readdir(d)) != nullptr) {
2864
std::string name = e->d_name;
2965
if (name == "." || name == "..") continue;
30-
std::string child = path + "/" + name;
31-
if (isDir(child)) rmrf(child);
32-
else ::remove(child.c_str());
66+
if (!rmrfImpl(path + "/" + name, visited, depth + 1)) ok = false;
3367
}
3468
closedir(d);
35-
return rmdir(path.c_str()) == 0;
69+
if (rmdir(path.c_str()) != 0) ok = false;
70+
return ok;
71+
}
72+
73+
static bool rmrf(const std::string& path) {
74+
std::set<VisitedKey> visited;
75+
return rmrfImpl(path, visited, 0);
3676
}
3777

3878
static std::vector<std::string> listDirs(const std::string& path) {
@@ -115,6 +155,17 @@ bool InstalledScanner::setActive(InstalledMod& mod, bool active) {
115155

116156
mkdirp(dstDir);
117157

158+
// Some platforms refuse rename() if dst exists. If a stale folder is in
159+
// the way (e.g. previous failed activation), remove it first.
160+
struct stat st;
161+
if (stat(dst.c_str(), &st) == 0) {
162+
LOG_WARN("InstalledScanner: rename target exists, removing first: %s", dst.c_str());
163+
if (!rmrf(dst)) {
164+
LOG_ERROR("InstalledScanner: could not clear rename target: %s", dst.c_str());
165+
return false;
166+
}
167+
}
168+
118169
if (rename(src.c_str(), dst.c_str()) != 0) {
119170
LOG_ERROR("InstalledScanner: rename failed: %s -> %s", src.c_str(), dst.c_str());
120171
return false;

0 commit comments

Comments
 (0)