1 /* nbdkit
2 * Copyright (C) 2013-2020 Red Hat Inc.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 *
15 * * Neither the name of Red Hat nor the names of its contributors may be
16 * used to endorse or promote products derived from this software without
17 * specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
31 */
32
33 #include <config.h>
34
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <assert.h>
38 #include <errno.h>
39
40 #include <nbdkit-plugin.h>
41
42 #include <ruby.h>
43 #ifdef HAVE_RUBY_VERSION_H
44 #include <ruby/version.h>
45 #endif
46
47 static VALUE nbdkit_module = Qnil;
48 static int last_error;
49
50 static VALUE
set_error(VALUE self,VALUE arg)51 set_error (VALUE self, VALUE arg)
52 {
53 int err;
54 VALUE v;
55
56 if (TYPE(arg) == T_CLASS) {
57 v = rb_const_get (arg, rb_intern ("Errno"));
58 err = NUM2INT (v);
59 } else if (TYPE (arg) == T_OBJECT) {
60 v = rb_funcall (arg, rb_intern ("errno"), 0);
61 err = NUM2INT (v);
62 } else {
63 err = NUM2INT (arg);
64 }
65 last_error = err;
66 nbdkit_set_error (err);
67 return Qnil;
68 }
69
70 static void
plugin_rb_load(void)71 plugin_rb_load (void)
72 {
73 RUBY_INIT_STACK;
74 ruby_init ();
75 ruby_init_loadpath ();
76
77 nbdkit_module = rb_define_module ("Nbdkit");
78 rb_define_module_function (nbdkit_module, "set_error", set_error, 1);
79 }
80
81 /* https://stackoverflow.com/questions/11086549/how-to-rb-protect-everything-in-ruby */
82 #define MAX_ARGS 16
83 struct callback_data {
84 VALUE receiver; /* object being called */
85 ID method_id; /* method on object being called */
86 int argc; /* number of args */
87 VALUE argv[MAX_ARGS]; /* list of args */
88 };
89
90 static VALUE
callback_dispatch(VALUE datav)91 callback_dispatch (VALUE datav)
92 {
93 struct callback_data *data = (struct callback_data *) datav;
94 return rb_funcall2 (data->receiver, data->method_id, data->argc, data->argv);
95 }
96
97 enum exception_class {
98 NO_EXCEPTION = 0,
99 EXCEPTION_NO_METHOD_ERROR,
100 EXCEPTION_OTHER,
101 };
102
103 static VALUE
funcall2(VALUE receiver,ID method_id,int argc,volatile VALUE * argv,enum exception_class * exception_happened)104 funcall2 (VALUE receiver, ID method_id, int argc, volatile VALUE *argv,
105 enum exception_class *exception_happened)
106 {
107 struct callback_data data;
108 size_t i, len;
109 int state = 0;
110 volatile VALUE ret, exn, message, backtrace, b;
111
112 assert (argc <= MAX_ARGS);
113
114 data.receiver = receiver;
115 data.method_id = method_id;
116 data.argc = argc;
117 for (i = 0; i < argc; ++i)
118 data.argv[i] = argv[i];
119
120 ret = rb_protect (callback_dispatch, (VALUE) &data, &state);
121 if (state) {
122 /* An exception was thrown. Get the per-thread exception. */
123 exn = rb_errinfo ();
124
125 /* We treat NoMethodError specially. */
126 if (rb_obj_is_kind_of (exn, rb_eNoMethodError)) {
127 if (exception_happened)
128 *exception_happened = EXCEPTION_NO_METHOD_ERROR;
129 }
130 else {
131 if (exception_happened)
132 *exception_happened = EXCEPTION_OTHER;
133
134 /* Print the exception. */
135 message = rb_funcall (exn, rb_intern ("to_s"), 0);
136 nbdkit_error ("ruby: %s", StringValueCStr (message));
137
138 /* Try to print the backtrace (a list of strings) if it exists. */
139 backtrace = rb_funcall (exn, rb_intern ("backtrace"), 0);
140 if (! NIL_P (backtrace)) {
141 len = RARRAY_LEN (backtrace);
142 for (i = 0; i < len; ++i) {
143 b = rb_ary_entry (backtrace, i);
144 nbdkit_error ("ruby: frame #%zu %s", i, StringValueCStr (b));
145 }
146 }
147 }
148
149 /* Reset the current thread exception. */
150 rb_set_errinfo (Qnil);
151 return Qnil;
152 }
153 else {
154 if (exception_happened)
155 *exception_happened = NO_EXCEPTION;
156 return ret;
157 }
158 }
159
160 static const char *script = NULL;
161 static void *code = NULL;
162
163 static void
plugin_rb_unload(void)164 plugin_rb_unload (void)
165 {
166 if (ruby_cleanup (0) != 0)
167 nbdkit_error ("ruby_cleanup failed");
168 }
169
170 static void
plugin_rb_dump_plugin(void)171 plugin_rb_dump_plugin (void)
172 {
173 #ifdef RUBY_API_VERSION_MAJOR
174 printf ("ruby_api_version=%d", RUBY_API_VERSION_MAJOR);
175 #ifdef RUBY_API_VERSION_MINOR
176 printf (".%d", RUBY_API_VERSION_MINOR);
177 #ifdef RUBY_API_VERSION_TEENY
178 printf (".%d", RUBY_API_VERSION_TEENY);
179 #endif
180 #endif
181 printf ("\n");
182 #endif
183
184 if (!script)
185 return;
186
187 assert (code != NULL);
188
189 (void) funcall2 (Qnil, rb_intern ("dump_plugin"), 0, NULL, NULL);
190 }
191
192 static int
plugin_rb_config(const char * key,const char * value)193 plugin_rb_config (const char *key, const char *value)
194 {
195 /* The first parameter must be "script". */
196 if (!script) {
197 int state;
198
199 if (strcmp (key, "script") != 0) {
200 nbdkit_error ("the first parameter must be script=/path/to/ruby/script.rb");
201 return -1;
202 }
203 script = value;
204
205 nbdkit_debug ("ruby: loading script %s", script);
206
207 /* Load the Ruby script into the interpreter. */
208 const char *options[] = { "--", script };
209 code = ruby_options (sizeof options / sizeof options[0],
210 (char **) options);
211
212 /* Check if we managed to compile the Ruby script to code. */
213 if (!ruby_executable_node (code, &state)) {
214 nbdkit_error ("could not compile ruby script (%s, state=%d)",
215 script, state);
216 return -1;
217 }
218
219 /* Execute the Ruby script. */
220 state = ruby_exec_node (code);
221 if (state) {
222 nbdkit_error ("could not execute ruby script (%s, state=%d)",
223 script, state);
224 return -1;
225 }
226
227 return 0;
228 }
229 else {
230 volatile VALUE argv[2];
231 enum exception_class exception_happened;
232
233 argv[0] = rb_str_new2 (key);
234 argv[1] = rb_str_new2 (value);
235 (void) funcall2 (Qnil, rb_intern ("config"), 2, argv, &exception_happened);
236 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
237 /* No config method, emulate what core nbdkit does if the
238 * config callback is NULL.
239 */
240 nbdkit_error ("%s: this plugin does not need command line configuration",
241 script);
242 return -1;
243 }
244 else if (exception_happened == EXCEPTION_OTHER)
245 return -1;
246
247 return 0;
248 }
249 }
250
251 static int
plugin_rb_config_complete(void)252 plugin_rb_config_complete (void)
253 {
254 enum exception_class exception_happened;
255
256 if (!script) {
257 nbdkit_error ("the first parameter must be script=/path/to/ruby/script.rb");
258 return -1;
259 }
260
261 assert (code != NULL);
262
263 (void) funcall2 (Qnil, rb_intern ("config_complete"), 0, NULL,
264 &exception_happened);
265 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
266 return 0; /* no config_complete method defined, ignore */
267 else if (exception_happened == EXCEPTION_OTHER)
268 return -1;
269
270 return 0;
271 }
272
273 static void *
plugin_rb_open(int readonly)274 plugin_rb_open (int readonly)
275 {
276 volatile VALUE argv[1];
277 volatile VALUE rv;
278 enum exception_class exception_happened;
279
280 argv[0] = readonly ? Qtrue : Qfalse;
281 rv = funcall2 (Qnil, rb_intern ("open"), 1, argv, &exception_happened);
282 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
283 nbdkit_error ("%s: missing callback: %s", script, "open");
284 return NULL;
285 }
286 else if (exception_happened == EXCEPTION_OTHER)
287 return NULL;
288
289 return (void *) rv;
290 }
291
292 static void
plugin_rb_close(void * handle)293 plugin_rb_close (void *handle)
294 {
295 volatile VALUE argv[1];
296
297 argv[0] = (VALUE) handle;
298 (void) funcall2 (Qnil, rb_intern ("close"), 1, argv, NULL);
299 /* OK to ignore exceptions here, if they are important then an error
300 * was printed already.
301 */
302 }
303
304 static int64_t
plugin_rb_get_size(void * handle)305 plugin_rb_get_size (void *handle)
306 {
307 volatile VALUE argv[1];
308 volatile VALUE rv;
309 enum exception_class exception_happened;
310
311 argv[0] = (VALUE) handle;
312 rv = funcall2 (Qnil, rb_intern ("get_size"), 1, argv, &exception_happened);
313 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
314 nbdkit_error ("%s: missing callback: %s", script, "get_size");
315 return -1;
316 }
317 else if (exception_happened == EXCEPTION_OTHER)
318 return -1;
319
320 return NUM2ULL (rv);
321 }
322
323 static int
plugin_rb_pread(void * handle,void * buf,uint32_t count,uint64_t offset)324 plugin_rb_pread (void *handle, void *buf,
325 uint32_t count, uint64_t offset)
326 {
327 volatile VALUE argv[3];
328 volatile VALUE rv;
329 enum exception_class exception_happened;
330
331 argv[0] = (VALUE) handle;
332 argv[1] = ULL2NUM (count);
333 argv[2] = ULL2NUM (offset);
334 rv = funcall2 (Qnil, rb_intern ("pread"), 3, argv, &exception_happened);
335 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
336 nbdkit_error ("%s: missing callback: %s", script, "pread");
337 return -1;
338 }
339 else if (exception_happened == EXCEPTION_OTHER)
340 return -1;
341
342 if (RSTRING_LEN (rv) < count) {
343 nbdkit_error ("%s: byte array returned from pread is too small",
344 script);
345 return -1;
346 }
347
348 memcpy (buf, RSTRING_PTR (rv), count);
349 return 0;
350 }
351
352 static int
plugin_rb_pwrite(void * handle,const void * buf,uint32_t count,uint64_t offset)353 plugin_rb_pwrite (void *handle, const void *buf,
354 uint32_t count, uint64_t offset)
355 {
356 volatile VALUE argv[3];
357 enum exception_class exception_happened;
358
359 argv[0] = (VALUE) handle;
360 argv[1] = rb_str_new (buf, count);
361 argv[2] = ULL2NUM (offset);
362 (void) funcall2 (Qnil, rb_intern ("pwrite"), 3, argv, &exception_happened);
363 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
364 nbdkit_error ("%s: missing callback: %s", script, "pwrite");
365 return -1;
366 }
367 else if (exception_happened == EXCEPTION_OTHER)
368 return -1;
369
370 return 0;
371 }
372
373 static int
plugin_rb_flush(void * handle)374 plugin_rb_flush (void *handle)
375 {
376 volatile VALUE argv[1];
377 enum exception_class exception_happened;
378
379 argv[0] = (VALUE) handle;
380 (void) funcall2 (Qnil, rb_intern ("flush"), 1, argv, &exception_happened);
381 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
382 nbdkit_error ("%s: not implemented: %s", script, "flush");
383 return -1;
384 }
385 else if (exception_happened == EXCEPTION_OTHER)
386 return -1;
387
388 return 0;
389 }
390
391 static int
plugin_rb_trim(void * handle,uint32_t count,uint64_t offset)392 plugin_rb_trim (void *handle, uint32_t count, uint64_t offset)
393 {
394 volatile VALUE argv[3];
395 enum exception_class exception_happened;
396
397 argv[0] = (VALUE) handle;
398 argv[1] = ULL2NUM (count);
399 argv[2] = ULL2NUM (offset);
400 (void) funcall2 (Qnil, rb_intern ("trim"), 3, argv, &exception_happened);
401 if (exception_happened == EXCEPTION_NO_METHOD_ERROR) {
402 nbdkit_error ("%s: not implemented: %s", script, "trim");
403 return -1;
404 }
405 else if (exception_happened == EXCEPTION_OTHER)
406 return -1;
407
408 return 0;
409 }
410
411 static int
plugin_rb_zero(void * handle,uint32_t count,uint64_t offset,int may_trim)412 plugin_rb_zero (void *handle, uint32_t count, uint64_t offset, int may_trim)
413 {
414 volatile VALUE argv[4];
415 enum exception_class exception_happened;
416
417 argv[0] = (VALUE) handle;
418 argv[1] = ULL2NUM (count);
419 argv[2] = ULL2NUM (offset);
420 argv[3] = may_trim ? Qtrue : Qfalse;
421 last_error = 0;
422 (void) funcall2 (Qnil, rb_intern ("zero"), 4, argv, &exception_happened);
423 if (last_error == EOPNOTSUPP || last_error == ENOTSUP ||
424 exception_happened == EXCEPTION_NO_METHOD_ERROR) {
425 nbdkit_debug ("zero falling back to pwrite");
426 nbdkit_set_error (EOPNOTSUPP);
427 return -1;
428 }
429 else if (exception_happened == EXCEPTION_OTHER)
430 return -1;
431
432 return 0;
433 }
434
435 static int
plugin_rb_can_write(void * handle)436 plugin_rb_can_write (void *handle)
437 {
438 volatile VALUE argv[1];
439 volatile VALUE rv;
440 enum exception_class exception_happened;
441
442 argv[0] = (VALUE) handle;
443 rv = funcall2 (Qnil, rb_intern ("can_write"), 1, argv, &exception_happened);
444 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
445 /* Fall back to checking if the pwrite method exists. */
446 rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
447 2, ID2SYM (rb_intern ("pwrite")), Qtrue);
448 else if (exception_happened == EXCEPTION_OTHER)
449 return -1;
450
451 return RTEST (rv);
452 }
453
454 static int
plugin_rb_can_flush(void * handle)455 plugin_rb_can_flush (void *handle)
456 {
457 volatile VALUE argv[1];
458 volatile VALUE rv;
459 enum exception_class exception_happened;
460
461 argv[0] = (VALUE) handle;
462 rv = funcall2 (Qnil, rb_intern ("can_flush"), 1, argv, &exception_happened);
463 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
464 /* Fall back to checking if the flush method exists. */
465 rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
466 2, ID2SYM (rb_intern ("flush")), Qtrue);
467 else if (exception_happened == EXCEPTION_OTHER)
468 return -1;
469
470 return RTEST (rv);
471 }
472
473 static int
plugin_rb_is_rotational(void * handle)474 plugin_rb_is_rotational (void *handle)
475 {
476 volatile VALUE argv[1];
477 volatile VALUE rv;
478 enum exception_class exception_happened;
479
480 argv[0] = (VALUE) handle;
481 rv = funcall2 (Qnil, rb_intern ("is_rotational"), 1, argv,
482 &exception_happened);
483 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
484 return 0;
485 else if (exception_happened == EXCEPTION_OTHER)
486 return -1;
487
488 return RTEST (rv);
489 }
490
491 static int
plugin_rb_can_trim(void * handle)492 plugin_rb_can_trim (void *handle)
493 {
494 volatile VALUE argv[1];
495 volatile VALUE rv;
496 enum exception_class exception_happened;
497
498 argv[0] = (VALUE) handle;
499 rv = funcall2 (Qnil, rb_intern ("can_trim"), 1, argv, &exception_happened);
500 if (exception_happened == EXCEPTION_NO_METHOD_ERROR)
501 /* Fall back to checking if the trim method exists. */
502 rv = rb_funcall (Qnil, rb_intern ("respond_to?"),
503 2, ID2SYM (rb_intern ("trim")), Qtrue);
504 else if (exception_happened == EXCEPTION_OTHER)
505 return -1;
506
507 return RTEST (rv);
508 }
509
510 #define plugin_rb_config_help \
511 "script=<FILENAME> (required) The Ruby plugin to run.\n" \
512 "[other arguments may be used by the plugin that you load]"
513
514 /* Ruby is inherently unsafe to call in parallel from multiple
515 * threads.
516 */
517 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_ALL_REQUESTS
518
519 static struct nbdkit_plugin plugin = {
520 .name = "ruby",
521 .version = PACKAGE_VERSION,
522
523 .load = plugin_rb_load,
524 .unload = plugin_rb_unload,
525 .dump_plugin = plugin_rb_dump_plugin,
526
527 .config = plugin_rb_config,
528 .config_complete = plugin_rb_config_complete,
529 .config_help = plugin_rb_config_help,
530
531 .open = plugin_rb_open,
532 .close = plugin_rb_close,
533
534 .get_size = plugin_rb_get_size,
535 .can_write = plugin_rb_can_write,
536 .can_flush = plugin_rb_can_flush,
537 .is_rotational = plugin_rb_is_rotational,
538 .can_trim = plugin_rb_can_trim,
539
540 .pread = plugin_rb_pread,
541 .pwrite = plugin_rb_pwrite,
542 .flush = plugin_rb_flush,
543 .trim = plugin_rb_trim,
544 .zero = plugin_rb_zero,
545 };
546
547 NBDKIT_REGISTER_PLUGIN(plugin)
548