Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 154 additions & 10 deletions core/logic/ExtensionSys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
#include <stdlib.h>

#include <memory>
#include <algorithm>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

#include "ExtensionSys.h"
#include <ILibrarySys.h>
Expand Down Expand Up @@ -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<std::string, size_t> load_order;
std::unordered_set<std::string> was_running;
std::vector<PluginInfo> 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<CPlugin *>(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<CPlugin *> dependents_copy = m_Dependents;
m_Dependents.clear();
for (List<CPlugin *>::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);
Comment thread
Kenzzer marked this conversation as resolved.

// 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<String>::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<std::pair<std::string, PluginType>> batch;
for (auto &info : to_reload)
batch.push_back({info.filename, info.type});

std::vector<CPlugin *> results = g_PluginSys.LoadPluginBatch(batch);
Comment thread
Kenzzer marked this conversation as resolved.

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()
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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");
}

Expand Down
52 changes: 52 additions & 0 deletions core/logic/PluginSys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,58 @@ void CPluginManager::LoadAll_SecondPass()
m_AllPluginsLoaded = true;
}

std::vector<CPlugin *> CPluginManager::LoadPluginBatch(
const std::vector<std::pair<std::string, PluginType>> &plugins)
{
std::vector<CPlugin *> 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();
}
Comment thread
Kenzzer marked this conversation as resolved.

return results;
}

bool CPluginManager::FindOrRequirePluginDeps(CPlugin *pPlugin)
{
struct _pl
Expand Down
11 changes: 11 additions & 0 deletions core/logic/PluginSys.h
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,17 @@ class CPluginManager :
void _SetPauseState(CPlugin *pPlugin, bool pause);

void ForEachPlugin(ke::Function<void(CPlugin *)> 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<CPlugin *> LoadPluginBatch(
Comment thread
bottiger1 marked this conversation as resolved.
const std::vector<std::pair<std::string, PluginType>> &plugins);
private:
LoadRes LoadPlugin(CPlugin **pPlugin, const char *path, bool debug, PluginType type);

Expand Down