Skip to content

Commit 52b14ea

Browse files
committed
Add reloading extensions by name
1 parent ce8125b commit 52b14ea

4 files changed

Lines changed: 256 additions & 11 deletions

File tree

core/logic/ExtensionSys.cpp

Lines changed: 189 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
#include <stdlib.h>
3333

3434
#include <memory>
35+
#include <algorithm>
36+
#include <string>
37+
#include <unordered_map>
38+
#include <unordered_set>
39+
#include <vector>
3540

3641
#include "ExtensionSys.h"
3742
#include <ILibrarySys.h>
@@ -250,15 +255,166 @@ bool CRemoteExtension::Reload(char *error, size_t maxlength)
250255
return false;
251256
}
252257

258+
namespace {
259+
struct ReloadPluginInfo {
260+
std::string filename;
261+
PluginType type;
262+
size_t order;
263+
bool was_paused;
264+
};
265+
266+
struct PendingExtensionReload {
267+
CLocalExtension *ext;
268+
std::vector<ReloadPluginInfo> to_reload;
269+
};
270+
} // anonymous namespace
271+
272+
void CExtensionManager::ProcessReloadFrame(void *data)
273+
{
274+
PendingExtensionReload *pending = static_cast<PendingExtensionReload *>(data);
275+
276+
// Bail if it was unloaded in the gap between frames.
277+
if (g_Extensions.m_Libs.find(pending->ext) == g_Extensions.m_Libs.end())
278+
{
279+
delete pending;
280+
return;
281+
}
282+
283+
// Reopen now that the deferred dlclose has run, so static initializers re-run.
284+
char error[256];
285+
if (!pending->ext->Load(error, sizeof(error)))
286+
{
287+
rootmenu->ConsolePrint("[SM] Failed to reload extension \"%s\": %s",
288+
pending->ext->GetFilename(), error);
289+
delete pending;
290+
return;
291+
}
292+
293+
// Reload dependent plugins in original load order.
294+
std::vector<std::pair<std::string, PluginType>> batch;
295+
for (auto &info : pending->to_reload)
296+
batch.push_back({info.filename, info.type});
297+
298+
std::vector<CPlugin *> results = g_PluginSys.LoadPluginBatch(batch);
299+
300+
for (size_t i = 0; i < pending->to_reload.size(); i++) {
301+
if (!results[i]) {
302+
rootmenu->ConsolePrint("[SM] Failed to reload plugin \"%s\"",
303+
pending->to_reload[i].filename.c_str());
304+
} else {
305+
rootmenu->ConsolePrint("[SM] Reloaded plugin \"%s\"",
306+
pending->to_reload[i].filename.c_str());
307+
if (pending->to_reload[i].was_paused)
308+
results[i]->SetPauseState(true);
309+
}
310+
}
311+
312+
rootmenu->ConsolePrint("[SM] Extension \"%s\" reloaded.", pending->ext->GetFilename());
313+
delete pending;
314+
}
315+
253316
bool CLocalExtension::Reload(char *error, size_t maxlength)
254317
{
255318
if (m_pLib == NULL) // FIXME: just load it instead?
256319
return false;
257-
320+
321+
// Step 1: Build a load-order map and identify direct dependents.
322+
// We must save this before any cleanup since unloading plugins removes them from
323+
// m_Dependents via DropRefsTo.
324+
std::unordered_map<std::string, size_t> load_order;
325+
std::unordered_set<std::string> was_running;
326+
std::vector<ReloadPluginInfo> to_reload;
327+
328+
{
329+
AutoPluginList list(scripts);
330+
for (size_t i = 0; i < list->size(); i++) {
331+
SMPlugin *plugin = list->at(i);
332+
std::string filename(plugin->GetFilename());
333+
load_order[filename] = i;
334+
335+
PluginStatus status = plugin->GetStatus();
336+
if (status == Plugin_Running || status == Plugin_Paused)
337+
was_running.insert(filename);
338+
339+
CPlugin *cp = static_cast<CPlugin *>(plugin);
340+
if (m_Dependents.find(cp) != m_Dependents.end() &&
341+
(status == Plugin_Running || status == Plugin_Paused))
342+
{
343+
to_reload.push_back({filename, plugin->GetType(), i, status == Plugin_Paused});
344+
}
345+
}
346+
}
347+
348+
// Step 2: Clear native cache entries and unbind weak refs.
349+
DropEverything();
350+
351+
// Step 3: Unload direct dependent plugins. They hold JIT-baked stale function
352+
// pointers that will crash if called after dlclose.
353+
// Copy and clear m_Dependents first — UnloadPlugin triggers OnPluginDestroyed
354+
// which calls DropRefsTo, removing entries from m_Dependents during iteration.
355+
// (UnloadExtension avoids this by removing itself from m_Libs first, but we
356+
// can't do that since we need to stay in m_Libs for reload.)
357+
List<CPlugin *> dependents_copy = m_Dependents;
358+
m_Dependents.clear();
359+
for (List<CPlugin *>::iterator p_iter = dependents_copy.begin();
360+
p_iter != dependents_copy.end();
361+
p_iter++)
362+
{
363+
scripts->UnloadPlugin((*p_iter));
364+
}
365+
366+
// Step 3b: Collect and unload cascaded victims — plugins that entered Plugin_Error
367+
// because they depended on a plugin we just unloaded (not on this extension directly).
368+
bool found;
369+
do {
370+
found = false;
371+
AutoPluginList list(scripts);
372+
for (size_t i = 0; i < list->size(); i++) {
373+
SMPlugin *plugin = list->at(i);
374+
std::string filename(plugin->GetFilename());
375+
if (plugin->GetStatus() == Plugin_Error && was_running.count(filename)) {
376+
auto it = load_order.find(filename);
377+
size_t order = (it != load_order.end()) ? it->second : SIZE_MAX;
378+
to_reload.push_back({filename, plugin->GetType(), order, false});
379+
was_running.erase(filename);
380+
scripts->UnloadPlugin(plugin);
381+
found = true;
382+
break; // List was modified, restart scan.
383+
}
384+
}
385+
} while (found);
386+
387+
// Sort by original load order so inter-plugin dependencies resolve correctly.
388+
std::sort(to_reload.begin(), to_reload.end(),
389+
[](const ReloadPluginInfo &a, const ReloadPluginInfo &b) {
390+
return a.order < b.order;
391+
});
392+
393+
// Step 4: Clean up extension state to prevent duplicates on reload.
394+
g_ShareSys.RemoveInterfaces(this);
395+
for (List<String>::iterator s_iter = m_Libraries.begin();
396+
s_iter != m_Libraries.end();
397+
s_iter++)
398+
{
399+
scripts->OnLibraryAction((*s_iter).c_str(), LibraryAction_Removed);
400+
}
401+
m_Libraries.clear();
402+
m_Interfaces.clear();
403+
404+
// Step 5: Unload the extension (request dlclose).
258405
m_pAPI->OnExtensionUnload();
259406
Unload();
260-
261-
return Load(error, maxlength);
407+
408+
// Step 6: Defer the reopen. Metamod doesn't actually dlclose until the current
409+
// command's hook stack unwinds, so reopening here would re-dlopen the still
410+
// mapped image and skip static re-init. Reopen on the next frame instead.
411+
PendingExtensionReload *pending = new PendingExtensionReload();
412+
pending->ext = this;
413+
pending->to_reload = std::move(to_reload);
414+
415+
g_pSM->AddFrameAction(&CExtensionManager::ProcessReloadFrame, pending);
416+
417+
return true;
262418
}
263419

264420
bool CRemoteExtension::IsExternal()
@@ -1188,18 +1344,39 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand
11881344
{
11891345
if (argcount < 4)
11901346
{
1191-
rootmenu->ConsolePrint("[SM] Usage: sm exts reload <#>");
1347+
rootmenu->ConsolePrint("[SM] Usage: sm exts reload <# or file>");
11921348
return;
11931349
}
1194-
1350+
11951351
const char *arg = command->Arg(3);
11961352
unsigned int num = atoi(arg);
1197-
CExtension *pExt = FindByOrder(num);
1353+
CExtension *pExt;
11981354

1199-
if (!pExt)
1355+
if (num != 0)
12001356
{
1201-
rootmenu->ConsolePrint("[SM] Extension number %d was not found.", num);
1202-
return;
1357+
pExt = FindByOrder(num);
1358+
if (!pExt)
1359+
{
1360+
rootmenu->ConsolePrint("[SM] Extension number %d was not found.", num);
1361+
return;
1362+
}
1363+
}
1364+
else
1365+
{
1366+
char path[PLATFORM_MAX_PATH];
1367+
ke::SafeSprintf(path, sizeof(path), "%s%s", arg, !strstr(arg, ".ext") ? ".ext" : "");
1368+
1369+
/* Strip platform extension if present, m_File doesn't include it. */
1370+
const char *ext = libsys->GetFileExtension(path);
1371+
if (ext && strcmp(ext, PLATFORM_LIB_EXT) == 0)
1372+
path[strlen(path) - strlen(PLATFORM_LIB_EXT) - 1] = '\0';
1373+
1374+
pExt = (CExtension *)FindExtensionByFile(path);
1375+
if (!pExt)
1376+
{
1377+
rootmenu->ConsolePrint("[SM] Extension %s is not loaded.", path);
1378+
return;
1379+
}
12031380
}
12041381

12051382
if (pExt->IsLoaded())
@@ -1211,7 +1388,8 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand
12111388

12121389
if (pExt->Reload(error, sizeof(error)))
12131390
{
1214-
rootmenu->ConsolePrint("[SM] Extension %s is now reloaded.", filename);
1391+
// Reopen is deferred a frame; ProcessReloadFrame prints completion.
1392+
rootmenu->ConsolePrint("[SM] Reloading extension %s...", filename);
12151393
}
12161394
else
12171395
{
@@ -1234,7 +1412,7 @@ void CExtensionManager::OnRootConsoleCommand(const char *cmdname, const ICommand
12341412
rootmenu->DrawGenericOption("info", "Extra extension information");
12351413
rootmenu->DrawGenericOption("list", "List extensions");
12361414
rootmenu->DrawGenericOption("load", "Load an extension");
1237-
rootmenu->DrawGenericOption("reload", "Reload an extension");
1415+
rootmenu->DrawGenericOption("reload", "Reload an extension by # or file");
12381416
rootmenu->DrawGenericOption("unload", "Unload an extension");
12391417
}
12401418

core/logic/ExtensionSys.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ class CExtensionManager :
183183
public:
184184
CExtension *GetExtensionFromIdent(IdentityToken_t *ptr);
185185
void Shutdown();
186+
187+
/* Frame action that completes a deferred reload: reopens the extension and
188+
* reloads its dependents. See CLocalExtension::Reload. */
189+
static void ProcessReloadFrame(void *data);
186190
CNativeOwner *GetNativeOwner(IExtension *pExt)
187191
{
188192
CExtension *p = (CExtension *)pExt;

core/logic/PluginSys.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,58 @@ void CPluginManager::LoadAll_SecondPass()
10841084
m_AllPluginsLoaded = true;
10851085
}
10861086

1087+
std::vector<CPlugin *> CPluginManager::LoadPluginBatch(
1088+
const std::vector<std::pair<std::string, PluginType>> &plugins)
1089+
{
1090+
std::vector<CPlugin *> results(plugins.size(), nullptr);
1091+
1092+
// First pass: compile and prepare all plugins.
1093+
for (size_t i = 0; i < plugins.size(); i++) {
1094+
auto &[filename, type] = plugins[i];
1095+
CPlugin *pl;
1096+
LoadRes res = LoadPlugin(&pl, filename.c_str(), true, type);
1097+
if (res == LoadRes_Failure) {
1098+
g_Logger.LogError("[SM] Failed to load plugin \"%s\": %s",
1099+
filename.c_str(), pl->GetErrorMsg());
1100+
delete pl;
1101+
continue;
1102+
}
1103+
if (res == LoadRes_AlreadyLoaded) {
1104+
results[i] = pl;
1105+
continue;
1106+
}
1107+
if (res == LoadRes_NeverLoad) {
1108+
continue;
1109+
}
1110+
AddPlugin(pl);
1111+
results[i] = pl;
1112+
}
1113+
1114+
// Second pass: resolve dependencies and call OnPluginStart for all
1115+
// newly loaded plugins. This matches the LoadAll_SecondPass pattern
1116+
// where all plugins are present in m_plugins before any RunSecondPass.
1117+
for (size_t i = 0; i < results.size(); i++) {
1118+
CPlugin *pl = results[i];
1119+
if (!pl || pl->GetStatus() != Plugin_Loaded)
1120+
continue;
1121+
if (!RunSecondPass(pl)) {
1122+
g_Logger.LogError("[SM] Unable to load plugin \"%s\": %s",
1123+
pl->GetFilename(), pl->GetErrorMsg());
1124+
Purge(pl);
1125+
pl->FinishEviction();
1126+
results[i] = nullptr;
1127+
}
1128+
}
1129+
1130+
// Final pass: OnAllPluginsLoaded.
1131+
for (auto *pl : results) {
1132+
if (pl && pl->GetStatus() <= Plugin_Paused)
1133+
pl->Call_OnAllPluginsLoaded();
1134+
}
1135+
1136+
return results;
1137+
}
1138+
10871139
bool CPluginManager::FindOrRequirePluginDeps(CPlugin *pPlugin)
10881140
{
10891141
struct _pl

core/logic/PluginSys.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,17 @@ class CPluginManager :
436436
void _SetPauseState(CPlugin *pPlugin, bool pause);
437437

438438
void ForEachPlugin(ke::Function<void(CPlugin *)> callback);
439+
440+
/**
441+
* Batch-loads plugins using the two-pass approach (compile all, then
442+
* run second pass for all) so that inter-plugin dependencies — including
443+
* circular ones — resolve the same way they do during initial load.
444+
*
445+
* Returns a vector of CPlugin pointers in the same order as the input.
446+
* Entries are nullptr for plugins that failed to load.
447+
*/
448+
std::vector<CPlugin *> LoadPluginBatch(
449+
const std::vector<std::pair<std::string, PluginType>> &plugins);
439450
private:
440451
LoadRes LoadPlugin(CPlugin **pPlugin, const char *path, bool debug, PluginType type);
441452

0 commit comments

Comments
 (0)