diff options
Diffstat (limited to 'plugin/src/plugin.c')
-rw-r--r-- | plugin/src/plugin.c | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/plugin/src/plugin.c b/plugin/src/plugin.c new file mode 100644 index 0000000..f65132f --- /dev/null +++ b/plugin/src/plugin.c | |||
@@ -0,0 +1,250 @@ | |||
1 | #include "plugin.h" | ||
2 | |||
3 | #include "cstring.h" | ||
4 | #include "list.h" | ||
5 | #include "log/log.h" // TODO: Use the error library instead. Move it to clib. | ||
6 | |||
7 | #include <assert.h> | ||
8 | #include <stdbool.h> | ||
9 | #include <stdlib.h> | ||
10 | #include <string.h> | ||
11 | |||
12 | #include <errno.h> | ||
13 | #include <linux/limits.h> | ||
14 | #include <poll.h> | ||
15 | #include <sys/inotify.h> | ||
16 | #include <unistd.h> | ||
17 | |||
18 | // Watching for IN_CREATE leads the plugin engine to try to reload a plugin's | ||
19 | // shared library before the compiler has fully written to it. | ||
20 | static const int WATCH_MASK = IN_CLOSE_WRITE; | ||
21 | |||
22 | typedef struct Plugin { | ||
23 | void* handle; // First member so that Plugin can be cast to handle. | ||
24 | void* state; // Plugin's internal state. | ||
25 | bool reloaded; // Whether the plugin has been reloaded state needs to be | ||
26 | // re-created. | ||
27 | PluginEngine* eng; // So that the public API can do stuff with just a Plugin*. | ||
28 | mstring filename; | ||
29 | } Plugin; | ||
30 | |||
31 | DEF_LIST(Plugin); | ||
32 | |||
33 | typedef struct PluginEngine { | ||
34 | int inotify_instance; | ||
35 | int dir_watch; // inotify watch on the plugins directory. | ||
36 | Plugin_list plugins; | ||
37 | mstring plugins_dir; | ||
38 | } PluginEngine; | ||
39 | |||
40 | // ----------------------------------------------------------------------------- | ||
41 | // Plugin. | ||
42 | // ----------------------------------------------------------------------------- | ||
43 | |||
44 | static mstring plugin_lib_name(const Plugin* plugin) { | ||
45 | return mstring_concat( | ||
46 | mstring_make("lib"), mstring_concat_cstr(plugin->filename, ".so")); | ||
47 | } | ||
48 | |||
49 | static mstring plugin_lib_path(const Plugin* plugin) { | ||
50 | return mstring_concat(plugin->eng->plugins_dir, plugin_lib_name(plugin)); | ||
51 | } | ||
52 | |||
53 | static bool load_library(Plugin* plugin) { | ||
54 | assert(plugin); | ||
55 | assert(plugin->eng); | ||
56 | |||
57 | // Handle reloading a previously-loaded library. | ||
58 | if (plugin->handle) { | ||
59 | dlclose(plugin->handle); | ||
60 | plugin->handle = 0; | ||
61 | } | ||
62 | |||
63 | const mstring lib = plugin_lib_path(plugin); | ||
64 | |||
65 | // If the plugin fails to load, make sure to keep the plugin's old handle to | ||
66 | // handle the error gracefully. This handles reload failures, specifically. | ||
67 | void* handle = 0; | ||
68 | if ((handle = dlopen(mstring_cstr(&lib), RTLD_NOW))) { | ||
69 | LOGD("Plugin [%s] loaded successfully", mstring_cstr(&plugin->filename)); | ||
70 | plugin->handle = handle; | ||
71 | return true; | ||
72 | } else { | ||
73 | LOGE("dlopen() failed: %s", dlerror()); | ||
74 | } | ||
75 | |||
76 | return false; | ||
77 | } | ||
78 | |||
79 | static void destroy_plugin(Plugin* plugin) { | ||
80 | if (plugin) { | ||
81 | if (plugin->handle) { | ||
82 | dlclose(plugin->handle); | ||
83 | plugin->handle = 0; | ||
84 | } | ||
85 | if (plugin->state) { | ||
86 | free(plugin->state); | ||
87 | plugin->state = 0; | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | |||
92 | Plugin* load_plugin(PluginEngine* eng, const char* filename) { | ||
93 | assert(eng); | ||
94 | assert(filename); | ||
95 | |||
96 | Plugin plugin = (Plugin){.eng = eng, .filename = mstring_make(filename)}; | ||
97 | |||
98 | if (!load_library(&plugin)) { | ||
99 | return 0; | ||
100 | } | ||
101 | |||
102 | list_push(eng->plugins, plugin); | ||
103 | return &eng->plugins.head->val; | ||
104 | } | ||
105 | |||
106 | void delete_plugin(Plugin** pPlugin) { | ||
107 | assert(pPlugin); | ||
108 | Plugin* plugin = *pPlugin; | ||
109 | if (plugin) { | ||
110 | assert(plugin->eng); | ||
111 | destroy_plugin(plugin); | ||
112 | list_remove_ptr(plugin->eng->plugins, plugin); | ||
113 | *pPlugin = 0; | ||
114 | } | ||
115 | } | ||
116 | |||
117 | static void delete_plugin_state(Plugin* plugin) { | ||
118 | if (plugin->state) { | ||
119 | free(plugin->state); | ||
120 | plugin->state = 0; | ||
121 | } | ||
122 | } | ||
123 | |||
124 | void set_plugin_state(Plugin* plugin, void* state) { | ||
125 | assert(plugin); | ||
126 | delete_plugin_state(plugin); | ||
127 | plugin->state = state; | ||
128 | } | ||
129 | |||
130 | void* get_plugin_state(Plugin* plugin) { | ||
131 | assert(plugin); | ||
132 | return plugin->state; | ||
133 | } | ||
134 | |||
135 | bool plugin_reloaded(Plugin* plugin) { | ||
136 | assert(plugin); | ||
137 | const bool reloaded = plugin->reloaded; | ||
138 | plugin->reloaded = false; | ||
139 | return reloaded; | ||
140 | } | ||
141 | |||
142 | // ----------------------------------------------------------------------------- | ||
143 | // Plugin Engine. | ||
144 | // ----------------------------------------------------------------------------- | ||
145 | |||
146 | PluginEngine* new_plugin_engine(const PluginEngineDesc* desc) { | ||
147 | PluginEngine* eng = 0; | ||
148 | |||
149 | if (!(eng = calloc(1, sizeof(PluginEngine)))) { | ||
150 | goto cleanup; | ||
151 | } | ||
152 | eng->plugins = make_list(Plugin); | ||
153 | eng->plugins_dir = mstring_concat_cstr(mstring_make(desc->plugins_dir), "/"); | ||
154 | |||
155 | LOGD("Watch plugins directory: %s", mstring_cstr(&eng->plugins_dir)); | ||
156 | |||
157 | if ((eng->inotify_instance = inotify_init()) == -1) { | ||
158 | LOGE("Failed to create inotify instance"); | ||
159 | goto cleanup; | ||
160 | } | ||
161 | if ((eng->dir_watch = inotify_add_watch( | ||
162 | eng->inotify_instance, mstring_cstr(&eng->plugins_dir), | ||
163 | WATCH_MASK)) == -1) { | ||
164 | LOGE("Failed to watch directory: %s", mstring_cstr(&eng->plugins_dir)); | ||
165 | goto cleanup; | ||
166 | } | ||
167 | |||
168 | return eng; | ||
169 | |||
170 | cleanup: | ||
171 | delete_plugin_engine(&eng); | ||
172 | return 0; | ||
173 | } | ||
174 | |||
175 | void delete_plugin_engine(PluginEngine** pEng) { | ||
176 | assert(pEng); | ||
177 | PluginEngine* eng = *pEng; | ||
178 | if (eng) { | ||
179 | list_foreach_mut(eng->plugins, { destroy_plugin(value); }); | ||
180 | del_list(eng->plugins); | ||
181 | if (eng->dir_watch != -1) { | ||
182 | inotify_rm_watch(eng->dir_watch, eng->inotify_instance); | ||
183 | close(eng->dir_watch); | ||
184 | eng->dir_watch = 0; | ||
185 | } | ||
186 | if (eng->inotify_instance != -1) { | ||
187 | close(eng->inotify_instance); | ||
188 | } | ||
189 | free(eng); | ||
190 | *pEng = 0; | ||
191 | } | ||
192 | } | ||
193 | |||
194 | void plugin_engine_update(PluginEngine* eng) { | ||
195 | assert(eng); | ||
196 | |||
197 | struct pollfd pollfds[1] = { | ||
198 | {eng->inotify_instance, POLLIN, 0} | ||
199 | }; | ||
200 | |||
201 | int ret = 0; | ||
202 | while ((ret = poll(pollfds, 1, 0)) != 0) { | ||
203 | if (ret > 0) { | ||
204 | const struct pollfd* pfd = &pollfds[0]; | ||
205 | if (pfd->revents & POLLIN) { | ||
206 | // inotify instances don't like to be partially read, and the events, | ||
207 | // when watching a directory, have a variable-length file name. | ||
208 | uint8_t buf[sizeof(struct inotify_event) + NAME_MAX + 1] = {0}; | ||
209 | ssize_t length = read(eng->inotify_instance, &buf, sizeof(buf)); | ||
210 | if (length == -1) { | ||
211 | LOGE( | ||
212 | "read() on inotify instance failed with error [%d]: %s", errno, | ||
213 | strerror(errno)); | ||
214 | break; | ||
215 | } | ||
216 | const uint8_t* next = buf; | ||
217 | const uint8_t* end = buf + sizeof(buf); | ||
218 | while (next < end) { | ||
219 | const struct inotify_event* event = (const struct inotify_event*)next; | ||
220 | if (event->mask & WATCH_MASK) { | ||
221 | if (event->wd == eng->dir_watch) { | ||
222 | if (event->len > 0) { | ||
223 | // Name does not include directory, e.g., libfoo.so | ||
224 | const mstring file = mstring_make(event->name); | ||
225 | list_foreach_mut(eng->plugins, { | ||
226 | Plugin* plugin = value; | ||
227 | if (mstring_eq(file, plugin_lib_name(plugin))) { | ||
228 | if (load_library(plugin)) { | ||
229 | plugin->reloaded = true; | ||
230 | } | ||
231 | break; | ||
232 | } | ||
233 | }); | ||
234 | } | ||
235 | } | ||
236 | } | ||
237 | next += sizeof(struct inotify_event) + event->len; | ||
238 | } | ||
239 | } | ||
240 | if ((pfd->revents & POLLERR) || (pfd->revents & POLLHUP) || | ||
241 | (pfd->revents & POLLNVAL)) { | ||
242 | LOGE("inotify instance is in a bad state"); | ||
243 | break; | ||
244 | } | ||
245 | } else if (ret == -1) { | ||
246 | LOGE("poll() failed with error [%d]: %s", errno, strerror(errno)); | ||
247 | break; | ||
248 | } | ||
249 | } | ||
250 | } | ||