diff --git a/core/logic/ExtensionSys.cpp b/core/logic/ExtensionSys.cpp index 4406a7943a..a3a4609c81 100644 --- a/core/logic/ExtensionSys.cpp +++ b/core/logic/ExtensionSys.cpp @@ -32,6 +32,11 @@ #include #include +#include +#include +#include +#include +#include #include "ExtensionSys.h" #include @@ -254,11 +259,129 @@ bool CLocalExtension::Reload(char *error, size_t maxlength) { if (m_pLib == NULL) // FIXME: just load it instead? return false; - + + // Step 1: Build a load-order map and identify direct dependents. + // We must save this before any cleanup since unloading plugins removes them from + // m_Dependents via DropRefsTo. + struct PluginInfo { + std::string filename; + PluginType type; + size_t order; + bool was_paused; + }; + + std::unordered_map load_order; + std::unordered_set was_running; + std::vector to_reload; + + { + AutoPluginList list(scripts); + for (size_t i = 0; i < list->size(); i++) { + SMPlugin *plugin = list->at(i); + std::string filename(plugin->GetFilename()); + load_order[filename] = i; + + PluginStatus status = plugin->GetStatus(); + if (status == Plugin_Running || status == Plugin_Paused) + was_running.insert(filename); + + CPlugin *cp = static_cast(plugin); + if (m_Dependents.find(cp) != m_Dependents.end() && + (status == Plugin_Running || status == Plugin_Paused)) + { + to_reload.push_back({filename, plugin->GetType(), i, status == Plugin_Paused}); + } + } + } + + // Step 2: Clear native cache entries and unbind weak refs. + DropEverything(); + + // Step 3: Unload direct dependent plugins. They hold JIT-baked stale function + // pointers that will crash if called after dlclose. + // Copy and clear m_Dependents first — UnloadPlugin triggers OnPluginDestroyed + // which calls DropRefsTo, removing entries from m_Dependents during iteration. + // (UnloadExtension avoids this by removing itself from m_Libs first, but we + // can't do that since we need to stay in m_Libs for reload.) + List dependents_copy = m_Dependents; + m_Dependents.clear(); + for (List::iterator p_iter = dependents_copy.begin(); + p_iter != dependents_copy.end(); + p_iter++) + { + scripts->UnloadPlugin((*p_iter)); + } + + // Step 3b: Collect and unload cascaded victims — plugins that entered Plugin_Error + // because they depended on a plugin we just unloaded (not on this extension directly). + bool found; + do { + found = false; + AutoPluginList list(scripts); + for (size_t i = 0; i < list->size(); i++) { + SMPlugin *plugin = list->at(i); + std::string filename(plugin->GetFilename()); + if (plugin->GetStatus() == Plugin_Error && was_running.count(filename)) { + auto it = load_order.find(filename); + size_t order = (it != load_order.end()) ? it->second : SIZE_MAX; + to_reload.push_back({filename, plugin->GetType(), order, false}); + was_running.erase(filename); + scripts->UnloadPlugin(plugin); + found = true; + break; // List was modified, restart scan. + } + } + } while (found); + + // Sort by original load order so inter-plugin dependencies resolve correctly. + std::sort(to_reload.begin(), to_reload.end(), + [](const PluginInfo &a, const PluginInfo &b) { + return a.order < b.order; + }); + + // Step 4: Clean up extension state to prevent duplicates on reload. + g_ShareSys.RemoveInterfaces(this); + for (List::iterator s_iter = m_Libraries.begin(); + s_iter != m_Libraries.end(); + s_iter++) + { + scripts->OnLibraryAction((*s_iter).c_str(), LibraryAction_Removed); + } + m_Libraries.clear(); + m_Interfaces.clear(); + + // Step 5: Unload the extension (dlclose). m_pAPI->OnExtensionUnload(); Unload(); - - return Load(error, maxlength); + + // Step 6: Reload the extension (dlopen). This calls OnExtensionLoad which + // re-registers natives, interfaces, and libraries. + if (!Load(error, maxlength)) + return false; + + // Step 7: Batch reload dependent plugins in original m_plugins order. + // Uses two-pass loading (compile all, then resolve dependencies) so that + // inter-plugin dependencies — including circular ones — resolve the same + // way they do during initial load. + std::vector> batch; + for (auto &info : to_reload) + batch.push_back({info.filename, info.type}); + + std::vector results = g_PluginSys.LoadPluginBatch(batch); + + for (size_t i = 0; i < to_reload.size(); i++) { + if (!results[i]) { + rootmenu->ConsolePrint("[SM] Failed to reload plugin \"%s\"", + to_reload[i].filename.c_str()); + } else { + rootmenu->ConsolePrint("[SM] Reloaded plugin \"%s\"", + to_reload[i].filename.c_str()); + if (to_reload[i].was_paused) + results[i]->SetPauseState(true); + } + } + + return true; } bool CRemoteExtension::IsExternal() @@ -1188,18 +1311,39 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand { if (argcount < 4) { - rootmenu->ConsolePrint("[SM] Usage: sm exts reload <#>"); + rootmenu->ConsolePrint("[SM] Usage: sm exts reload <# or file>"); return; } - + const char *arg = command->Arg(3); unsigned int num = atoi(arg); - CExtension *pExt = FindByOrder(num); + CExtension *pExt; - if (!pExt) + if (num != 0) { - rootmenu->ConsolePrint("[SM] Extension number %d was not found.", num); - return; + pExt = FindByOrder(num); + if (!pExt) + { + rootmenu->ConsolePrint("[SM] Extension number %d was not found.", num); + return; + } + } + else + { + char path[PLATFORM_MAX_PATH]; + ke::SafeSprintf(path, sizeof(path), "%s%s", arg, !strstr(arg, ".ext") ? ".ext" : ""); + + /* Strip platform extension if present, m_File doesn't include it. */ + const char *ext = libsys->GetFileExtension(path); + if (ext && strcmp(ext, PLATFORM_LIB_EXT) == 0) + path[strlen(path) - strlen(PLATFORM_LIB_EXT) - 1] = '\0'; + + pExt = (CExtension *)FindExtensionByFile(path); + if (!pExt) + { + rootmenu->ConsolePrint("[SM] Extension %s is not loaded.", path); + return; + } } if (pExt->IsLoaded()) @@ -1234,7 +1378,7 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand rootmenu->DrawGenericOption("info", "Extra extension information"); rootmenu->DrawGenericOption("list", "List extensions"); rootmenu->DrawGenericOption("load", "Load an extension"); - rootmenu->DrawGenericOption("reload", "Reload an extension"); + rootmenu->DrawGenericOption("reload", "Reload an extension by # or file"); rootmenu->DrawGenericOption("unload", "Unload an extension"); } diff --git a/core/logic/PluginSys.cpp b/core/logic/PluginSys.cpp index 74d4161dc3..91ed450c9f 100644 --- a/core/logic/PluginSys.cpp +++ b/core/logic/PluginSys.cpp @@ -1084,6 +1084,58 @@ void CPluginManager::LoadAll_SecondPass() m_AllPluginsLoaded = true; } +std::vector CPluginManager::LoadPluginBatch( + const std::vector> &plugins) +{ + std::vector results(plugins.size(), nullptr); + + // First pass: compile and prepare all plugins. + for (size_t i = 0; i < plugins.size(); i++) { + auto &[filename, type] = plugins[i]; + CPlugin *pl; + LoadRes res = LoadPlugin(&pl, filename.c_str(), true, type); + if (res == LoadRes_Failure) { + g_Logger.LogError("[SM] Failed to load plugin \"%s\": %s", + filename.c_str(), pl->GetErrorMsg()); + delete pl; + continue; + } + if (res == LoadRes_AlreadyLoaded) { + results[i] = pl; + continue; + } + if (res == LoadRes_NeverLoad) { + continue; + } + AddPlugin(pl); + results[i] = pl; + } + + // Second pass: resolve dependencies and call OnPluginStart for all + // newly loaded plugins. This matches the LoadAll_SecondPass pattern + // where all plugins are present in m_plugins before any RunSecondPass. + for (size_t i = 0; i < results.size(); i++) { + CPlugin *pl = results[i]; + if (!pl || pl->GetStatus() != Plugin_Loaded) + continue; + if (!RunSecondPass(pl)) { + g_Logger.LogError("[SM] Unable to load plugin \"%s\": %s", + pl->GetFilename(), pl->GetErrorMsg()); + Purge(pl); + pl->FinishEviction(); + results[i] = nullptr; + } + } + + // Final pass: OnAllPluginsLoaded. + for (auto *pl : results) { + if (pl && pl->GetStatus() <= Plugin_Paused) + pl->Call_OnAllPluginsLoaded(); + } + + return results; +} + bool CPluginManager::FindOrRequirePluginDeps(CPlugin *pPlugin) { struct _pl diff --git a/core/logic/PluginSys.h b/core/logic/PluginSys.h index 1afbf52170..66bcce6dfe 100644 --- a/core/logic/PluginSys.h +++ b/core/logic/PluginSys.h @@ -436,6 +436,17 @@ class CPluginManager : void _SetPauseState(CPlugin *pPlugin, bool pause); void ForEachPlugin(ke::Function callback); + + /** + * Batch-loads plugins using the two-pass approach (compile all, then + * run second pass for all) so that inter-plugin dependencies — including + * circular ones — resolve the same way they do during initial load. + * + * Returns a vector of CPlugin pointers in the same order as the input. + * Entries are nullptr for plugins that failed to load. + */ + std::vector LoadPluginBatch( + const std::vector> &plugins); private: LoadRes LoadPlugin(CPlugin **pPlugin, const char *path, bool debug, PluginType type);