VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
PluginLoader.cpp
Go to the documentation of this file.
2
4
9#include <algorithm>
10#include <format>
11#include <ranges>
12#include <utility>
13
14namespace VoltMod
15{
16
17#if defined(_WIN32)
18static constexpr std::string_view LibrarySuffix = ".dll";
19#else
20static constexpr std::string_view LibrarySuffix = ".so";
21#endif
22
23// Leftovers are a plugin bug; the host has already dropped them.
24static void WarnLeaks(std::string_view name, const LeakReport& leaks)
25{
26 for (std::string_view event : leaks.Subscriptions)
27 {
28 Log::Warn("{} left a {} subscription behind; the host dropped it.", name, event);
29 }
30 for (const std::string& service : leaks.Services)
31 {
32 Log::Warn("{} left the service '{}' published; the host withdrew it.", name, service);
33 }
34 for (const uint64_t addonId : leaks.Addons)
35 {
36 Log::Warn("{} left workshop addon {} required; the host released it.", name, addonId);
37 }
38}
39
40static std::filesystem::path LibraryPath(std::string_view name)
41{
42 return ResolvePath(PluginFile(name, std::format("{}{}", name, LibrarySuffix)));
43}
44
46{
47 return InstalledPlugins::Discover(ResolvePath("addons/voltmod/plugins"));
48}
49
51
56
58{
60 LoadGroup(installed, [](std::string_view) { return true; });
61 Log::Info("{} of {} installed plugin(s) loaded.", _loaded.size(),
62 installed.Plugins.size() + installed.Refused.size());
63}
64
66{
67 _pending.clear();
68 while (!_loaded.empty())
69 {
70 UnloadOne(_loaded.back().Manifest.Name, UnloadTime::Shutdown);
71 }
72}
73
74void PluginLoader::Defer(ActionKind kind, std::string_view name)
75{
76 _pending.push_back({.Kind = kind, .Name = std::string(name)});
77 Log::Info("Queued {} of '{}' for the next frame.", Strings::ToLower(Name(kind)), name);
78}
79
81{
82 if (_pending.empty())
83 {
84 return;
85 }
86
87 // Take the list aside: an action that defers another leaves it for the next frame.
88 const std::vector<PendingAction> actions = std::move(_pending);
89 _pending.clear();
90
91 for (const PendingAction& action : actions)
92 {
93 switch (action.Kind)
94 {
96 RunLoad(action.Name);
97 break;
99 RunUnload(action.Name);
100 break;
102 RunReload(action.Name);
103 break;
104 }
105 }
106}
107
108void PluginLoader::LoadGroup(const InstalledScan& installed, const std::function<bool(std::string_view)>& wanted)
109{
111 for (const std::vector<RefusedPlugin>* refusals : {&installed.Refused, &list.Refused})
112 {
113 for (const RefusedPlugin& refused : *refusals)
114 {
115 if (wanted(refused.Name))
116 {
117 Refuse(refused.Name, refused.Reason.Detail);
118 }
119 }
120 }
121
122 for (const std::string& name : list.ToLoad)
123 {
124 if (!wanted(name) || FindLoaded(name) != nullptr)
125 {
126 continue;
127 }
128
129 const auto found = std::ranges::find(installed.Plugins, name, &PluginManifest::Name);
130 if (Status loaded = LoadOne(*found); !loaded)
131 {
132 Refuse(name, loaded.error().Detail);
133 }
134 }
135}
136
137void PluginLoader::Refuse(std::string_view name, std::string reason)
138{
139 Log::Error("Refusing '{}': {}", name, reason);
140 _refused.insert_or_assign(std::string(name), std::move(reason));
141}
142
143Status PluginLoader::LoadOne(const PluginManifest& manifest)
144{
145 const std::string& name = manifest.Name;
146
148 if (!code)
149 {
150 return std::unexpected(code.error());
151 }
152
153 const Result<void*> entry = code->Symbol(PluginEntryName);
154 if (!entry)
155 {
156 return std::unexpected(entry.error());
157 }
158
159 using EntryPoint = const PluginDescriptor* (*)();
160 const PluginDescriptor* descriptor = reinterpret_cast<EntryPoint>(*entry)();
162 {
163 return valid;
164 }
165
166 PluginContext* view = _registry.AddPlugin(name, manifest.LogTag, manifest.Version);
167 if (view == nullptr)
168 {
169 return std::unexpected(Error::Failed("the host already holds a view under that name"));
170 }
171 view->SetMinLogLevel(manifest.LogLevel);
172 _assets.Mount(name);
173
174 char failure[512] = {};
175 if (!descriptor->Load(view, failure, sizeof failure))
176 {
177 failure[sizeof failure - 1] = '\0';
178 // A refusing plugin has torn itself down; its library is freed when `code` leaves scope.
179 WarnLeaks(name, _registry.RemovePlugin(name));
180 _assets.Unmount(name);
181 return std::unexpected(Error::Failed(failure[0] != '\0' ? failure : "its Load returned false"));
182 }
183 // After Load, so a replayed player meets a plugin whose commands and handlers are all in.
184 view->ReplayMissedEvents();
185
186 _loaded.push_back({.Manifest = manifest, .Descriptor = descriptor, .Library = std::move(*code)});
187 _refused.erase(name);
188
189 Log::Info("Loaded {} v{}.", name, manifest.Version);
190 return {};
191}
192
193void PluginLoader::UnloadOne(std::string_view name, UnloadTime when)
194{
195 // Copy it: the view may point into the record this is about to erase.
196 const std::string plugin(name);
197
198 const auto found =
199 std::ranges::find_if(_loaded, [&](const LoadedPlugin& loaded) { return loaded.Manifest.Name == plugin; });
200 if (found == _loaded.end())
201 {
202 return;
203 }
204
205 if (when == UnloadTime::MidMap)
206 {
207 _registry.FindPlugin(plugin)->DisconnectClients();
208 }
209 found->Descriptor->Unload();
210 WarnLeaks(plugin, _registry.RemovePlugin(plugin));
211 _assets.Unmount(plugin);
212
213 // Free the library last: its hook thunks and closures live in it until Unload has run.
214 _loaded.erase(found);
215 Log::Info("Unloaded {}.", plugin);
216}
217
218LoadedPlugin* PluginLoader::FindLoaded(std::string_view name)
219{
220 const auto found =
221 std::ranges::find_if(_loaded, [name](const LoadedPlugin& plugin) { return plugin.Manifest.Name == name; });
222 return found != _loaded.end() ? &*found : nullptr;
223}
224
226{
227 LoadedPlugin* plugin = FindLoaded(name);
228 if (plugin == nullptr)
229 {
230 Log::Warn("'{}' is not loaded.", name);
231 }
232 return plugin;
233}
234
235std::vector<PluginManifest> PluginLoader::LoadedManifests() const
236{
237 std::vector<PluginManifest> manifests;
238 manifests.reserve(_loaded.size());
239 for (const LoadedPlugin& plugin : _loaded)
240 {
241 manifests.push_back(plugin.Manifest);
242 }
243 return manifests;
244}
245
246void PluginLoader::RunLoad(std::string_view name)
247{
248 if (FindLoaded(name) != nullptr)
249 {
250 Log::Warn("'{}' is already loaded.", name);
251 return;
252 }
253
254 const InstalledScan installed = Installed();
255 if (const auto broken = std::ranges::find(installed.Refused, name, &RefusedPlugin::Name);
256 broken != installed.Refused.end())
257 {
258 Refuse(name, broken->Reason.Detail);
259 return;
260 }
261
262 const auto found = std::ranges::find(installed.Plugins, name, &PluginManifest::Name);
263 if (found == installed.Plugins.end())
264 {
265 Log::Warn("'{}' is not installed.", name);
266 return;
267 }
268
269 for (const std::string& dependency : found->Dependencies)
270 {
271 if (FindLoaded(dependency) == nullptr)
272 {
273 Refuse(name, std::format("it requires '{}', which is not loaded.", dependency));
274 return;
275 }
276 }
277
278 if (Status loaded = LoadOne(*found); !loaded)
279 {
280 Refuse(name, loaded.error().Detail);
281 }
282}
283
284void PluginLoader::RunUnload(std::string_view name)
285{
286 if (RequireLoaded(name) == nullptr)
287 {
288 return;
289 }
290
291 const std::vector<std::string> dependents = PluginDependencies::RequiredDependents(name, LoadedManifests());
292 if (!dependents.empty())
293 {
294 Log::Warn("Refusing to unload '{}': {} still requires it.", name, Strings::Join(dependents, ", "));
295 return;
296 }
297
298 UnloadOne(name, UnloadTime::MidMap);
299}
300
301void PluginLoader::RunReload(std::string_view name)
302{
303 if (RequireLoaded(name) == nullptr)
304 {
305 return;
306 }
307
308 // Whatever requires it goes down and comes back with it, each one before what it requires.
309 std::vector<std::string> group = PluginDependencies::RequiredDependents(name, LoadedManifests());
310 group.emplace_back(name);
311
312 std::vector<std::string> going;
313 for (const LoadedPlugin& plugin : _loaded | std::views::reverse)
314 {
315 if (std::ranges::find(group, plugin.Manifest.Name) != group.end())
316 {
317 going.push_back(plugin.Manifest.Name);
318 }
319 }
320
321 for (const std::string& plugin : going)
322 {
323 UnloadOne(plugin, UnloadTime::MidMap);
324 }
325
326 // Read the manifests again: a rebuilt plugin may declare different dependencies.
327 LoadGroup(Installed(),
328 [&group](std::string_view plugin) { return std::ranges::find(group, plugin) != group.end(); });
329}
330
331} // namespace VoltMod
Enumerator names without a hand-written switch.
LoadedPlugin * RequireLoaded(std::string_view name)
PluginLoader(PluginRegistry &registry, ServerAssets &assets)
void Defer(ActionKind kind, std::string_view name)
The loaded plugins and the engine events raised on them, one per process.
PluginContext * FindPlugin(std::string_view name)
LeakReport RemovePlugin(std::string_view name)
PluginContext * AddPlugin(std::string_view name, std::string_view logTag={}, std::string_view version={})
void Mount(std::string_view plugin)
void Unmount(std::string_view plugin)
static Result< SharedLibrary > Open(const std::filesystem::path &path)
static std::string Join(const std::vector< std::string > &parts, std::string_view delimiter)
Definition Strings.cpp:114
static std::string ToLower(std::string_view str)
Definition Strings.cpp:100
InstalledScan Discover(const std::filesystem::path &plugins)
void Error(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:97
void Info(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:79
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
LoadList Resolve(std::span< const PluginManifest > installed)
Decide which of installed the host loads.
std::vector< std::string > RequiredDependents(std::string_view plugin, std::span< const PluginManifest > loaded)
std::filesystem::path ResolvePath(std::string_view relativePath)
a relative path against the base directory. If the path is already absolute, returns it as-is.
Definition Paths.cpp:14
static constexpr std::string_view LibrarySuffix
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static std::filesystem::path LibraryPath(std::string_view name)
Status ValidateDescriptor(const PluginDescriptor *descriptor, std::string_view hostVersion)
std::string PluginFile(std::string_view plugin, std::string_view relative)
"addons/voltmod/plugins/<plugin>/<relative>".
Definition Paths.cpp:25
std::expected< void, Error > Status
Definition Result.hpp:67
constexpr const char * PluginEntryName
static void WarnLeaks(std::string_view name, const LeakReport &leaks)
static InstalledScan Installed()
static Error Failed(std::string detail)
Definition Result.hpp:59