Skip to content

Commit de0cf8d

Browse files
committed
Add remote commands that pass the arguments of another process to the running plugins
1 parent 98f67fd commit de0cf8d

8 files changed

Lines changed: 146 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ The lifecycle has the following ordering and ownership rules:
436436
437437
- Loading and initialization follow dependency order. `loadOrder()` returns that order after loading, with only the plugins that took part.
438438
- `pluginsInitialized()` and `aboutToShutdown()` run in reverse dependency order.
439+
- `PluginSystem::remoteCommand()` passes the arguments of another process of the application, such as the files of a second start, to `IPlugin::remoteCommand()` of every running plugin in load order. A command passed before `loadPlugins()` finishes is kept until every plugin is running. A command passed after shutdown starts has no effect.
439440
- Shutdown unloads libraries in reverse dependency order.
440441
- After a spec reaches a loaded lifecycle state, `PluginSpec::plugin()` returns its non-owning `IPlugin` pointer. Hosts can cast it to the IID-specific application interface.
441442
- Successful shutdown clears the plugin pointer when the library is unloaded.

‎include/stdcorelib/pluginsystem/iplugin.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
#ifndef STDCORELIB_PLUGINSYSTEM_IPLUGIN_H
44
#define STDCORELIB_PLUGINSYSTEM_IPLUGIN_H
55

6+
#include <filesystem>
67
#include <string>
8+
#include <vector>
79

810
#include <stdcorelib/plugin/plugin.h>
911

@@ -28,6 +30,17 @@ namespace stdc::pluginsystem {
2830

2931
/// Notifies this plugin during shutdown before any plugin library is unloaded.
3032
virtual void aboutToShutdown();
33+
34+
/// Handles the arguments that another process of the application passes to this
35+
/// process instead of running itself, such as the files of a second start.
36+
///
37+
/// \param arguments The arguments in UTF-8, without the program.
38+
/// \param workingDirectory The working directory of the other process, against which a
39+
/// relative path in \a arguments resolves.
40+
///
41+
/// \sa PluginSystem::remoteCommand()
42+
virtual void remoteCommand(const std::vector<std::string> &arguments,
43+
const std::filesystem::path &workingDirectory);
3144
};
3245

3346
/// @}

‎include/stdcorelib/pluginsystem/pluginsystem.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,17 @@ namespace stdc::pluginsystem {
133133
/// PluginSpec.
134134
void loadPlugins();
135135

136+
/// Passes the arguments of another process of the application to IPlugin::remoteCommand()
137+
/// of every running plugin, in load order.
138+
///
139+
/// - A command passed before loadPlugins() finishes is kept, and passed in the order of
140+
/// the commands once every plugin is initialized. This includes a command passed during
141+
/// loadPlugins() from an event loop that a plugin runs.
142+
/// - A command passed after shutdownPlugins() starts has no effect.
143+
/// - The caller is the thread that calls loadPlugins().
144+
void remoteCommand(const std::vector<std::string> &arguments,
145+
const std::filesystem::path &workingDirectory);
146+
136147
/// Shuts down and unloads plugins in reverse dependency order.
137148
///
138149
/// Repeated calls and calls before loadPlugins() have no effect. The destructor calls this

‎src/pluginsystem/iplugin.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ namespace stdc::pluginsystem {
1212
void IPlugin::aboutToShutdown() {
1313
}
1414

15+
void IPlugin::remoteCommand(const std::vector<std::string> &, const std::filesystem::path &) {
16+
}
17+
1518
}

‎src/pluginsystem/pluginsystem.cpp‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ namespace stdc::pluginsystem {
285285
return result;
286286
}
287287

288+
void PluginSystem::Impl::dispatchRemoteCommand(const RemoteCommand &command) {
289+
for (auto data : loadOrder) {
290+
if (shutdownFinished) {
291+
return;
292+
}
293+
if (data->state == PluginSpec::Running) {
294+
data->plugin->remoteCommand(command.arguments, command.workingDirectory);
295+
}
296+
}
297+
}
298+
288299
void PluginSystem::Impl::shutdownPlugins() {
289300
std::lock_guard<std::mutex> lifecycleLock(lifecycleMtx);
290301
if (!loadStarted || shutdownFinished) {
@@ -406,7 +417,7 @@ namespace stdc::pluginsystem {
406417

407418
void PluginSystem::loadPlugins() {
408419
stdc_impl_t;
409-
std::lock_guard<std::mutex> lifecycleLock(impl.lifecycleMtx);
420+
std::unique_lock<std::mutex> lifecycleLock(impl.lifecycleMtx);
410421
{
411422
std::unique_lock<std::shared_mutex> lock(impl.configMtx);
412423
if (impl.loadStarted) {
@@ -464,6 +475,32 @@ namespace stdc::pluginsystem {
464475
data->plugin->pluginsInitialized();
465476
data->state = PluginSpec::Running;
466477
}
478+
479+
// The kept commands are passed without the lifecycle lock, so that a plugin may shut
480+
// down the system while it handles a command. A command passed meanwhile is kept behind
481+
// them, because loadFinished is set only after the last of them.
482+
lifecycleLock.unlock();
483+
while (!impl.pendingRemoteCommands.empty()) {
484+
const auto command = std::move(impl.pendingRemoteCommands.front());
485+
impl.pendingRemoteCommands.erase(impl.pendingRemoteCommands.begin());
486+
impl.dispatchRemoteCommand(command);
487+
}
488+
impl.loadFinished = true;
489+
}
490+
491+
void PluginSystem::remoteCommand(const std::vector<std::string> &arguments,
492+
const std::filesystem::path &workingDirectory) {
493+
stdc_impl_t;
494+
// No lock, because a plugin may run an event loop that delivers the command during
495+
// loadPlugins(), which holds the lifecycle lock on the same thread.
496+
if (impl.shutdownFinished) {
497+
return;
498+
}
499+
if (!impl.loadFinished) {
500+
impl.pendingRemoteCommands.push_back({arguments, workingDirectory});
501+
return;
502+
}
503+
impl.dispatchRemoteCommand({arguments, workingDirectory});
467504
}
468505

469506
void PluginSystem::shutdownPlugins() {

‎src/pluginsystem/pluginsystem_p.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ namespace stdc::pluginsystem {
5050
bool loadStarted = false;
5151
bool shutdownFinished = false;
5252

53+
struct RemoteCommand {
54+
std::vector<std::string> arguments;
55+
std::filesystem::path workingDirectory;
56+
};
57+
/// Set when loadPlugins() has initialized every plugin and passed the kept commands. Read
58+
/// and written on the lifecycle thread only.
59+
bool loadFinished = false;
60+
/// The commands passed before loadPlugins() finished, in their order
61+
std::vector<RemoteCommand> pendingRemoteCommands;
62+
void dispatchRemoteCommand(const RemoteCommand &command);
63+
5364
std::map<PluginSpecData *, ResolvedDependencies> resolvedDependencies;
5465
/// Written once under configMtx. The lifecycle thread reads it without the lock.
5566
PluginOrder loadOrder;

‎tests/auto/plugins/test_pluginsystem_plugin/test_pluginsystem_lifecycle_plugin.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ namespace {
2727
notify("aboutToShutdown");
2828
}
2929

30+
// Records the arguments separated by spaces and the working directory after a colon
31+
void remoteCommand(const std::vector<std::string> &arguments,
32+
const std::filesystem::path &workingDirectory) override {
33+
std::string event = "remoteCommand";
34+
for (const auto &argument : arguments) {
35+
event += " " + argument;
36+
}
37+
event += ":" + workingDirectory.generic_string();
38+
notify(event.c_str());
39+
}
40+
3041
private:
3142
static void notify(const char *event) {
3243
if (eventCallback) {

‎tests/auto/test_pluginsystem.cpp‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <atomic>
1010
#include <chrono>
1111
#include <fstream>
12+
#include <functional>
1213
#include <thread>
1314

1415
#include <boost/test/unit_test.hpp>
@@ -163,9 +164,14 @@ namespace {
163164

164165
std::vector<std::string> lifecycleEvents;
165166
stdc::pluginsystem::PluginSystem *activePluginSystem = nullptr;
167+
// Called with each recorded event, so that a test may act during the lifecycle
168+
std::function<void(const std::string &)> lifecycleHook;
166169

167170
void recordLifecycleEvent(const char *plugin, const char *event) {
168171
lifecycleEvents.push_back(std::string(plugin) + "." + event);
172+
if (lifecycleHook) {
173+
lifecycleHook(lifecycleEvents.back());
174+
}
169175
if (activePluginSystem) {
170176
activePluginSystem->plugins();
171177
activePluginSystem->hasError();
@@ -602,6 +608,58 @@ BOOST_AUTO_TEST_CASE(test_lifecycle_dependency_order_and_reentrant_queries) {
602608
}
603609
}
604610

611+
BOOST_AUTO_TEST_CASE(test_remote_commands_reach_running_plugins_in_load_order) {
612+
TemporaryPluginSystemDirectory directory(stdc::pluginsystem::PluginSystem::Bundle, false);
613+
const auto providerPath = directory.addPlugin(
614+
"provider", R"({"id":"Provider","displayName":"Provider","version":"1.0"})",
615+
TEST_PLUGINSYSTEM_LIFECYCLE_PROVIDER_PATH);
616+
const auto consumerPath = directory.addPlugin(
617+
"consumer",
618+
R"({"id":"Consumer","displayName":"Consumer","version":"1.0","dependencies":[{"id":"Provider","version":"1.0","type":"required"}]})",
619+
TEST_PLUGINSYSTEM_LIFECYCLE_CONSUMER_PATH);
620+
621+
stdc::SharedLibrary providerLibrary;
622+
stdc::SharedLibrary consumerLibrary;
623+
setLifecycleCallback(&providerLibrary, providerPath);
624+
setLifecycleCallback(&consumerLibrary, consumerPath);
625+
626+
stdc::pluginsystem::PluginSystem system("org.stdcorelib.PluginSystem",
627+
stdc::pluginsystem::PluginSystem::Bundle);
628+
system.setPluginPaths(directory.path());
629+
lifecycleEvents.clear();
630+
631+
// A command before loading and a command during the initialization are kept until every
632+
// plugin is running, in their order.
633+
system.remoteCommand({"before"}, "/a");
634+
lifecycleHook = [&system](const std::string &event) {
635+
if (event == "consumer.initialize") {
636+
system.remoteCommand({"during", "x"}, "/b");
637+
}
638+
};
639+
system.loadPlugins();
640+
lifecycleHook = nullptr;
641+
system.remoteCommand({"after"}, "/c");
642+
system.shutdownPlugins();
643+
system.remoteCommand({"shutdown"}, "/d");
644+
645+
const std::vector<std::string> expected{
646+
"provider.initialize",
647+
"consumer.initialize",
648+
"consumer.pluginsInitialized",
649+
"provider.pluginsInitialized",
650+
"provider.remoteCommand before:/a",
651+
"consumer.remoteCommand before:/a",
652+
"provider.remoteCommand during x:/b",
653+
"consumer.remoteCommand during x:/b",
654+
"provider.remoteCommand after:/c",
655+
"consumer.remoteCommand after:/c",
656+
"consumer.aboutToShutdown",
657+
"provider.aboutToShutdown",
658+
};
659+
BOOST_CHECK_EQUAL_COLLECTIONS(lifecycleEvents.begin(), lifecycleEvents.end(), expected.begin(),
660+
expected.end());
661+
}
662+
605663
BOOST_AUTO_TEST_CASE(test_load_order_puts_dependencies_first) {
606664
TemporaryPluginSystemDirectory directory(stdc::pluginsystem::PluginSystem::Bundle, false);
607665
directory.addPlugin(

0 commit comments

Comments
 (0)