diff --git a/ext/lxc/extconf.rb b/ext/lxc/extconf.rb index 07fd4d4..5fb4f9a 100644 --- a/ext/lxc/extconf.rb +++ b/ext/lxc/extconf.rb @@ -3,5 +3,12 @@ require 'mkmf' abort 'missing liblxc' unless find_library('lxc', 'lxc_container_new') abort 'missing lxc/lxccontainer.h' unless have_header('lxc/lxccontainer.h') +def add_define(name) + $defs.push("-D#{name}") +end + +add_define "HAVE_RB_THREAD_CALL_WITHOUT_GVL" if have_func('rb_thread_call_without_gvl') +add_define "HAVE_RB_THREAD_BLOCKING_REGION" if have_func('rb_thread_blocking_region') + $CFLAGS += " -Wall #{ENV['CFLAGS']}" create_makefile('lxc/lxc') diff --git a/ext/lxc/lxc.c b/ext/lxc/lxc.c index 2d51776..136cd2d 100644 --- a/ext/lxc/lxc.c +++ b/ext/lxc/lxc.c @@ -5,9 +5,33 @@ #include #include #include +#include #define SYMBOL(s) ID2SYM(rb_intern(s)) +#if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) +// Defined in Ruby, but not in all Ruby versions' header files +extern void *rb_thread_call_without_gvl(void *(*func)(void *), void *data1, + rb_unblock_function_t *ubf, void *data2); +#define WITHOUT_GVL_RESULT_TYPE void * +#define WITHOUT_GVL_RESULT(__X__) (void *)(intptr_t)(__X__) +#define WITHOUT_GVL_CALL(__FUNC__, __ARG__) (int)(intptr_t)rb_thread_call_without_gvl(__FUNC__, __ARG__, NULL, NULL) +#define WITHOUT_GVL_CALL_NOOP(__FUNC__, __ARG__) rb_thread_call_without_gvl(__FUNC__, __ARG__, NULL, NULL) +#define WITHOUT_GVL_CALL2(__FUNC__, __ARG__, __KILLFUNC__, __KILLARG__) (int)(intptr_t)rb_thread_call_without_gvl(__FUNC__, __ARG__, __KILLFUNC__, __KILLARG__) +#elif defined(HAVE_RB_THREAD_BLOCKING_REGION) +#define WITHOUT_GVL_RESULT_TYPE VALUE +#define WITHOUT_GVL_RESULT(__X__) INT2NUM(__X__) +#define WITHOUT_GVL_CALL(__FUNC__, __ARG__) NUM2INT(rb_thread_blocking_region(__FUNC__, __ARG__, NULL, NULL)) +#define WITHOUT_GVL_CALL_NOOP(__FUNC__, __ARG__) rb_thread_blocking_region(__FUNC__, __ARG__, NULL, NULL) +#define WITHOUT_GVL_CALL2(__FUNC__, __ARG__, __KILLFUNC__, __KILLARG__) NUM2INT(rb_thread_blocking_region(__FUNC__, __ARG__, __KILLFUNC__, __KILLARG__)) +#else +#define WITHOUT_GVL_RESULT_TYPE int +#define WITHOUT_GVL_RESULT(__X__) __X__ +#define WITHOUT_GVL_CALL(__FUNC__, __ARG__) __FUNC__(__ARG__) +#define WITHOUT_GVL_CALL_NOOP(__FUNC__, __ARG__) __FUNC__(__ARG__) +#define WITHOUT_GVL_CALL2(__FUNC__, __ARG__, __KILLFUNC__, __KILLARG__) __FUNC__(__ARG__) +#endif + extern int lxc_wait_for_pid_status(pid_t pid); extern long lxc_config_parse_arch(const char *arch); @@ -161,6 +185,29 @@ lxc_version(VALUE self) return rb_str_new2(lxc_get_version()); } +struct list_containers_without_gvl_args +{ + int active; + int defined; + char *config; + char **names; +}; + +static WITHOUT_GVL_RESULT_TYPE +list_containers_without_gvl(void *args_void) +{ + struct list_containers_without_gvl_args *args = (struct list_containers_without_gvl_args *)args_void; + int num_containers = 0; + args->names = NULL; + if (args->active && args->defined) + num_containers = list_all_containers(args->config, &args->names, NULL); + else if (args->active) + num_containers = list_active_containers(args->config, &args->names, NULL); + else if (args->defined) + num_containers = list_defined_containers(args->config, &args->names, NULL); + return WITHOUT_GVL_RESULT(num_containers); +} + /* * call-seq: * LXC.list_containers([opts]) @@ -175,36 +222,33 @@ static VALUE lxc_list_containers(int argc, VALUE *argv, VALUE self) { int i, num_containers; - int active, defined; - char *config; - char **names; VALUE rb_active, rb_defined, rb_config; VALUE rb_opts; VALUE rb_containers; + struct list_containers_without_gvl_args args; rb_scan_args(argc, argv, "01", &rb_opts); - if (NIL_P(rb_opts)) { - active = 1; - defined = 1; - config = NULL; - } else { - Check_Type(rb_opts, T_HASH); - rb_active = rb_hash_aref(rb_opts, SYMBOL("active")); - active = (rb_active != Qnil) && (rb_active != Qfalse); - rb_defined = rb_hash_aref(rb_opts, SYMBOL("defined")); - defined = (rb_defined != Qnil) && (rb_defined != Qfalse); - rb_config = rb_hash_aref(rb_opts, SYMBOL("config_path")); - config = NIL_P(rb_config) ? NULL : StringValuePtr(rb_config); - } + args.active = 1; + args.defined = 1; + args.config = NULL; - num_containers = 0; - if (active && defined) - num_containers = list_all_containers(config, &names, NULL); - else if (active) - num_containers = list_active_containers(config, &names, NULL); - else if (defined) - num_containers = list_defined_containers(config, &names, NULL); + if (!NIL_P(rb_opts)) { + Check_Type(rb_opts, T_HASH); + + rb_active = rb_hash_aref(rb_opts, SYMBOL("active")); + if (!NIL_P(rb_active)) + args.active = rb_active != Qfalse; + + rb_defined = rb_hash_aref(rb_opts, SYMBOL("defined")); + if (!NIL_P(rb_defined)) + args.defined = rb_defined != Qfalse; + + rb_config = rb_hash_aref(rb_opts, SYMBOL("config_path")); + if (!NIL_P(rb_config)) + args.config = StringValuePtr(rb_config); + } + num_containers = WITHOUT_GVL_CALL(list_containers_without_gvl, &args); if (num_containers < 0) rb_raise(Error, "failure to list containers"); @@ -214,10 +258,10 @@ lxc_list_containers(int argc, VALUE *argv, VALUE self) * ie, don't use free_c_string_array(). */ for (i = 0; i < num_containers; i++) { - rb_ary_store(rb_containers, i, rb_str_new2(names[i])); - free(names[i]); + rb_ary_store(rb_containers, i, rb_str_new2(args.names[i])); + free(args.names[i]); } - free(names); + free(args.names); return rb_containers; } @@ -383,9 +427,23 @@ container_state(VALUE self) return rb_str_intern(rb_funcall(rb_state, rb_intern("downcase"), 0)); } +struct add_device_node_without_gvl_args +{ + struct container_data *data; + char *src_path; + char *dest_path; +}; + +static WITHOUT_GVL_RESULT_TYPE +add_device_node_without_gvl(void *args_void) +{ + struct add_device_node_without_gvl_args *args = (struct add_device_node_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->add_device_node(args->data->container, args->src_path, args->dest_path)); +} + /* * call-seq: - * container.add_device_node(src_path, dst_path = src_path) + * container.add_device_node(src_path, dest_path = src_path) * * Adds a device node to the container. */ @@ -393,17 +451,16 @@ static VALUE container_add_device_node(int argc, VALUE *argv, VALUE self) { int ret; - char *src_path, *dst_path; - struct container_data *data; - VALUE rb_src_path, rb_dst_path; + VALUE rb_src_path, rb_dest_path; + struct add_device_node_without_gvl_args args; - rb_scan_args(argc, argv, "11", &rb_src_path, &rb_dst_path); - src_path = NIL_P(rb_src_path) ? NULL : StringValuePtr(rb_src_path); - dst_path = NIL_P(rb_dst_path) ? NULL : StringValuePtr(rb_dst_path); + rb_scan_args(argc, argv, "11", &rb_src_path, &rb_dest_path); + args.src_path = StringValuePtr(rb_src_path); + args.dest_path = NIL_P(rb_dest_path) ? NULL : StringValuePtr(rb_dest_path); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->add_device_node(data->container, src_path, dst_path); + ret = WITHOUT_GVL_CALL(add_device_node_without_gvl, &args); if (!ret) rb_raise(Error, "unable to add device node"); @@ -593,6 +650,20 @@ err: return NULL; } +static WITHOUT_GVL_RESULT_TYPE +lxc_wait_for_pid_status_without_gvl(void *pid) +{ + return WITHOUT_GVL_RESULT(lxc_wait_for_pid_status(*(pid_t*)pid)); +} + +#if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) || defined(HAVE_RB_THREAD_BLOCKING_REGION) +static void +kill_pid_without_gvl(void *pid) +{ + kill(*(pid_t *)pid, SIGKILL); +} +#endif + /* * call-seq: * container.attach(opts = {}, &block) @@ -650,7 +721,8 @@ container_attach(int argc, VALUE *argv, VALUE self) goto out; if (wait) { - ret = lxc_wait_for_pid_status(pid); + ret = WITHOUT_GVL_CALL2(lxc_wait_for_pid_status_without_gvl, &pid, + kill_pid_without_gvl, &pid); /* handle case where attach fails */ if (WIFEXITED(ret) && WEXITSTATUS(ret) == 255) ret = -1; @@ -702,6 +774,30 @@ container_clear_config_item(VALUE self, VALUE rb_key) return self; } +struct clone_without_gvl_args +{ + struct container_data *data; + struct lxc_container *new_container; + char *name; + char *config_path; + int flags; + char *bdev_type; + char *bdev_data; + uint64_t new_size; + char **hook_args; +}; + +static WITHOUT_GVL_RESULT_TYPE +clone_without_gvl(void *args_void) +{ + struct clone_without_gvl_args *args = (struct clone_without_gvl_args *)args_void; + struct lxc_container *container = args->data->container; + args->new_container = container->clone(container, args->name, + args->config_path, args->flags, args->bdev_type, args->bdev_data, + args->new_size, args->hook_args); + return WITHOUT_GVL_RESULT(0); +} + /* * call-seq: * container.clone(clone_name, opts = {}) @@ -719,27 +815,22 @@ container_clear_config_item(VALUE self, VALUE rb_key) static VALUE container_clone(int argc, VALUE *argv, VALUE self) { - int flags; - uint64_t new_size; - char *name, *config_path, *bdev_type, *bdev_data; - char **hook_args; - struct lxc_container *container, *new_container; - struct container_data *data; VALUE rb_name, rb_opts; VALUE rb_flags, rb_config_path, rb_bdev_type, rb_bdev_data; VALUE rb_new_size, rb_hook_args; VALUE rb_args[2]; + struct clone_without_gvl_args args; rb_scan_args(argc, argv, "11", &rb_name, &rb_opts); - name = StringValuePtr(rb_name); + args.name = StringValuePtr(rb_name); - config_path = NULL; - flags = 0; - bdev_type = NULL; - bdev_data = NULL; - new_size = 0; - hook_args = NULL; + args.config_path = NULL; + args.flags = 0; + args.bdev_type = NULL; + args.bdev_data = NULL; + args.new_size = 0; + args.hook_args = NULL; rb_config_path = Qnil; @@ -747,49 +838,66 @@ container_clone(int argc, VALUE *argv, VALUE self) Check_Type(rb_opts, T_HASH); rb_config_path = rb_hash_aref(rb_opts, SYMBOL("config_path")); if (!NIL_P(rb_config_path)) - config_path = StringValuePtr(rb_config_path); + args.config_path = StringValuePtr(rb_config_path); rb_flags = rb_hash_aref(rb_opts, SYMBOL("flags")); if (!NIL_P(rb_flags)) - flags = NUM2INT(rb_flags); + args.flags = NUM2INT(rb_flags); rb_bdev_type = rb_hash_aref(rb_opts, SYMBOL("bdev_type")); if (!NIL_P(rb_bdev_type)) - bdev_type = StringValuePtr(rb_bdev_type); + args.bdev_type = StringValuePtr(rb_bdev_type); rb_bdev_data = rb_hash_aref(rb_opts, SYMBOL("bdev_data")); if (!NIL_P(rb_bdev_data)) - bdev_data = StringValuePtr(rb_bdev_data); + args.bdev_data = StringValuePtr(rb_bdev_data); rb_new_size = rb_hash_aref(rb_opts, SYMBOL("new_size")); if (!NIL_P(rb_bdev_data)) - new_size = NUM2ULL(rb_new_size); + args.new_size = NUM2ULL(rb_new_size); rb_hook_args = rb_hash_aref(rb_opts, SYMBOL("hook_args")); if (!NIL_P(rb_hook_args)) - hook_args = ruby_to_c_string_array(rb_hook_args); + args.hook_args = ruby_to_c_string_array(rb_hook_args); } - Data_Get_Struct(self, struct container_data, data); - container = data->container; + Data_Get_Struct(self, struct container_data, args.data); - new_container = container->clone(container, name, config_path, - flags, bdev_type, bdev_data, new_size, - hook_args); + WITHOUT_GVL_CALL_NOOP(clone_without_gvl, &args); - if (hook_args) - free_c_string_array(hook_args); + if (args.hook_args) + free_c_string_array(args.hook_args); - if (new_container == NULL) + if (args.new_container == NULL) rb_raise(Error, "unable to clone container"); - lxc_container_put(new_container); + lxc_container_put(args.new_container); rb_args[0] = rb_name; rb_args[1] = rb_config_path; return rb_class_new_instance(2, rb_args, Container); } +struct console_without_gvl_args +{ + struct container_data *data; + int tty_num; + int stdin_fd; + int stdout_fd; + int stderr_fd; + int escape; +}; + +static WITHOUT_GVL_RESULT_TYPE +console_without_gvl(void *args_void) +{ + struct console_without_gvl_args *args = (struct console_without_gvl_args *)args_void; + struct lxc_container *container = args->data->container; + return WITHOUT_GVL_RESULT(container->console(container, args->tty_num, + args->stdin_fd, args->stdout_fd, + args->stderr_fd, args->escape)); +} + /* * call-seq: * container.console(opts = {}) @@ -807,29 +915,44 @@ static VALUE container_console(int argc, VALUE *argv, VALUE self) { int ret; - int tty_num = -1, stdin_fd = 0, stdout_fd = 1, stderr_fd = 2, escape = 1; - struct container_data *data; - struct lxc_container *container; + struct console_without_gvl_args args; VALUE rb_opts; + VALUE rb_opt; + + args.tty_num = -1; + args.stdin_fd = 0; + args.stdout_fd = 1; + args.stderr_fd = 2; + args.escape = 1; rb_scan_args(argc, argv, "01", &rb_opts); switch (TYPE(rb_opts)) { case T_HASH: - tty_num = NUM2INT(rb_hash_aref(rb_opts, SYMBOL("tty_num"))); - stdin_fd = NUM2INT(rb_hash_aref(rb_opts, SYMBOL("stdin_fd"))); - stdout_fd = NUM2INT(rb_hash_aref(rb_opts, SYMBOL("stdout_fd"))); - stderr_fd = NUM2INT(rb_hash_aref(rb_opts, SYMBOL("stderr_fd"))); - escape = NUM2INT(rb_hash_aref(rb_opts, SYMBOL("escape"))); + rb_opt = rb_hash_aref(rb_opts, SYMBOL("tty_num")); + if (!NIL_P(rb_opt)) + args.tty_num = NUM2INT(rb_opt); + rb_opt = rb_hash_aref(rb_opts, SYMBOL("stdin_fd")); + if (!NIL_P(rb_opt)) + args.stdin_fd = NUM2INT(rb_opt); + rb_opt = rb_hash_aref(rb_opts, SYMBOL("stdout_fd")); + if (!NIL_P(rb_opt)) + args.stdout_fd = NUM2INT(rb_opt); + rb_opt = rb_hash_aref(rb_opts, SYMBOL("stderr_fd")); + if (!NIL_P(rb_opt)) + args.stderr_fd = NUM2INT(rb_opt); + rb_opt = rb_hash_aref(rb_opts, SYMBOL("escape")); + if (!NIL_P(rb_opt)) + args.escape = NUM2INT(rb_opt); + break; + case T_NIL: break; default: rb_raise(rb_eArgError, "options must be a hash"); } - Data_Get_Struct(self, struct container_data, data); - container = data->container; + Data_Get_Struct(self, struct container_data, args.data); - ret = container->console(container, tty_num, stdin_fd, stdout_fd, stderr_fd, - escape); + ret = WITHOUT_GVL_CALL(console_without_gvl, &args); if (ret != 0) rb_raise(Error, "unable to access container console"); @@ -863,6 +986,22 @@ container_console_fd(int argc, VALUE *argv, VALUE self) return rb_class_new_instance(1, rb_io_args, rb_cIO); } +/* Used to run container->create outside of GIL */ +struct container_create_without_gvl_args { + struct container_data *data; + char *template; + char *bdevtype; + int flags; + char **args; +}; + +static WITHOUT_GVL_RESULT_TYPE +container_create_without_gvl(void *args_void) +{ + struct container_create_without_gvl_args *args = (struct container_create_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->create(args->data->container, args->template, args->bdevtype, NULL, args->flags, args->args)); +} + /* * call-seq: * container.create(template, bdevtype = nil, flags = 0, args = []) @@ -876,28 +1015,26 @@ container_console_fd(int argc, VALUE *argv, VALUE self) static VALUE container_create(int argc, VALUE *argv, VALUE self) { - int ret, flags; - char *template; - char *bdevtype; - char **args = { NULL }; - struct container_data *data; - struct lxc_container *container; + int ret; VALUE rb_template, rb_bdevtype, rb_flags, rb_args; + struct container_create_without_gvl_args args; + char ** default_args = { NULL }; + args.args = default_args; rb_scan_args(argc, argv, "13", &rb_template, &rb_bdevtype, &rb_flags, &rb_args); - template = StringValuePtr(rb_template); - bdevtype = NIL_P(rb_bdevtype) ? NULL : StringValuePtr(rb_bdevtype); - flags = NIL_P(rb_flags) ? 0 : NUM2INT(rb_flags); + args.template = StringValuePtr(rb_template); + args.bdevtype = NIL_P(rb_bdevtype) ? NULL : StringValuePtr(rb_bdevtype); + args.flags = NIL_P(rb_flags) ? 0 : NUM2INT(rb_flags); if (!NIL_P(rb_args)) - args = ruby_to_c_string_array(rb_args); + args.args = ruby_to_c_string_array(rb_args); - Data_Get_Struct(self, struct container_data, data); - container = data->container; - ret = container->create(container, template, bdevtype, NULL, flags, args); + Data_Get_Struct(self, struct container_data, args.data); + + ret = WITHOUT_GVL_CALL(container_create_without_gvl, &args); if (!NIL_P(rb_args)) - free_c_string_array(args); + free_c_string_array(args.args); if (!ret) rb_raise(Error, "unable to create container"); @@ -905,6 +1042,14 @@ container_create(int argc, VALUE *argv, VALUE self) return self; } + +static WITHOUT_GVL_RESULT_TYPE +destroy_without_gvl(void *data_void) +{ + struct container_data *data = (struct container_data *)data_void; + return WITHOUT_GVL_RESULT(data->container->destroy(data->container)); +} + /* * call-seq: * container.destroy @@ -919,12 +1064,19 @@ container_destroy(VALUE self) Data_Get_Struct(self, struct container_data, data); - ret = data->container->destroy(data->container); + ret = WITHOUT_GVL_CALL(destroy_without_gvl, data); if (!ret) rb_raise(Error, "unable to destroy container"); return self; } +static WITHOUT_GVL_RESULT_TYPE +freeze_without_gvl(void *data_void) +{ + struct container_data *data = (struct container_data *)data_void; + return WITHOUT_GVL_RESULT(data->container->freeze(data->container)); +} + /* * call-seq: * container.freeze @@ -939,7 +1091,7 @@ container_freeze(VALUE self) Data_Get_Struct(self, struct container_data, data); - ret = data->container->freeze(data->container); + ret = WITHOUT_GVL_CALL(freeze_without_gvl, data); if (!ret) rb_raise(Error, "unable to freeze container"); @@ -1146,6 +1298,19 @@ container_ips(int argc, VALUE *argv, VALUE self) return rb_ips; } +struct load_config_without_gvl_args +{ + struct container_data *data; + char *path; +}; + +static WITHOUT_GVL_RESULT_TYPE +load_config_without_gvl(void *args_void) +{ + struct load_config_without_gvl_args *args = (struct load_config_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->load_config(args->data->container, args->path)); +} + /* * call-seq: * container.load_config(config_path = nil) @@ -1156,22 +1321,28 @@ static VALUE container_load_config(int argc, VALUE *argv, VALUE self) { int ret; - char *path; - struct container_data *data; VALUE rb_path; + struct load_config_without_gvl_args args; rb_scan_args(argc, argv, "01", &rb_path); - path = NIL_P(rb_path) ? NULL : StringValuePtr(rb_path); + args.path = NIL_P(rb_path) ? NULL : StringValuePtr(rb_path); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->load_config(data->container, path); + ret = WITHOUT_GVL_CALL(load_config_without_gvl, &args); if (!ret) rb_raise(Error, "unable to load configuration file"); return self; } +static WITHOUT_GVL_RESULT_TYPE +reboot_without_gvl(void* data_void) +{ + struct container_data *data = (struct container_data *)data_void; + return WITHOUT_GVL_RESULT(data->container->reboot(data->container)); +} + /* * call-seq: * container.reboot @@ -1186,16 +1357,30 @@ container_reboot(VALUE self) Data_Get_Struct(self, struct container_data, data); - ret = data->container->reboot(data->container); + ret = WITHOUT_GVL_CALL(reboot_without_gvl, data); if (!ret) rb_raise(Error, "unable to reboot container"); return self; } +struct remove_device_node_without_gvl_args +{ + struct container_data *data; + char *src_path; + char *dest_path; +}; + +static WITHOUT_GVL_RESULT_TYPE +remove_device_node_without_gvl(void *args_void) +{ + struct remove_device_node_without_gvl_args *args = (struct remove_device_node_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->remove_device_node(args->data->container, args->src_path, args->dest_path)); +} + /* * call-seq: - * container.remove_device_node(src_path, dst_path = src_path) + * container.remove_device_node(src_path, dest_path = src_path) * * Removes a device node from the container. */ @@ -1203,25 +1388,35 @@ static VALUE container_remove_device_node(int argc, VALUE *argv, VALUE self) { int ret; - char *src_path, *dst_path; - struct lxc_container *container; - struct container_data *data; - VALUE rb_src_path, rb_dst_path; + VALUE rb_src_path, rb_dest_path; + struct remove_device_node_without_gvl_args args; - rb_scan_args(argc, argv, "11", &rb_src_path, &rb_dst_path); - src_path = StringValuePtr(rb_src_path); - dst_path = NIL_P(rb_dst_path) ? NULL : StringValuePtr(rb_dst_path); + rb_scan_args(argc, argv, "11", &rb_src_path, &rb_dest_path); + args.src_path = StringValuePtr(rb_src_path); + args.dest_path = NIL_P(rb_dest_path) ? NULL : StringValuePtr(rb_dest_path); - Data_Get_Struct(self, struct container_data, data); - container = data->container; + Data_Get_Struct(self, struct container_data, args.data); - ret = container->remove_device_node(container, src_path, dst_path); + ret = WITHOUT_GVL_CALL(remove_device_node_without_gvl, &args); if (!ret) rb_raise(Error, "unable to remove device node"); return self; } +struct rename_without_gvl_args +{ + struct container_data *data; + char *name; +}; + +static WITHOUT_GVL_RESULT_TYPE +rename_without_gvl(void *args_void) +{ + struct rename_without_gvl_args *args = (struct rename_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->rename(args->data->container, args->name)); +} + /* * call-seq: * container.rename(new_name) @@ -1233,14 +1428,14 @@ static VALUE container_rename(VALUE self, VALUE rb_name) { int ret; - char *name; - struct container_data *data; VALUE rb_args[2]; + struct rename_without_gvl_args args; - name = StringValuePtr(rb_name); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->rename(data->container, name); + args.name = StringValuePtr(rb_name); + + ret = WITHOUT_GVL_CALL(rename_without_gvl, &args); if (!ret) rb_raise(Error, "unable to rename container"); @@ -1278,20 +1473,32 @@ container_running_config_item(VALUE self, VALUE rb_key) return rb_value; } +struct save_config_without_gvl_args +{ + struct container_data *data; + char *path; +}; + +static WITHOUT_GVL_RESULT_TYPE +save_config_without_gvl(void *args_void) +{ + struct save_config_without_gvl_args *args = (struct save_config_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->save_config(args->data->container, args->path)); +} + static VALUE container_save_config(int argc, VALUE *argv, VALUE self) { int ret; - char *path; - struct container_data *data; VALUE rb_path; + struct save_config_without_gvl_args args; rb_scan_args(argc, argv, "01", &rb_path); - path = NIL_P(rb_path) ? NULL : StringValuePtr(rb_path); + args.path = NIL_P(rb_path) ? NULL : StringValuePtr(rb_path); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->save_config(data->container, path); + ret = WITHOUT_GVL_CALL(save_config_without_gvl, &args); if (!ret) rb_raise(Error, "unable to save configuration file"); @@ -1391,6 +1598,19 @@ container_set_config_path(VALUE self, VALUE rb_path) return self; } +struct shutdown_without_gvl_args +{ + struct container_data *data; + int timeout; +}; + +static WITHOUT_GVL_RESULT_TYPE +shutdown_without_gvl(void* args_void) +{ + struct shutdown_without_gvl_args *args = (struct shutdown_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->shutdown(args->data->container, args->timeout)); +} + /* * call-seq: * container.shutdown(timeout = -1) @@ -1402,22 +1622,36 @@ container_set_config_path(VALUE self, VALUE rb_path) static VALUE container_shutdown(int argc, VALUE *argv, VALUE self) { - int ret, timeout; - struct container_data *data; + int ret; VALUE rb_timeout; + struct shutdown_without_gvl_args args; rb_scan_args(argc, argv, "01", &rb_timeout); - timeout = NIL_P(rb_timeout) ? -1 : NUM2INT(rb_timeout); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->shutdown(data->container, timeout); + args.timeout = NIL_P(rb_timeout) ? -1 : NUM2INT(rb_timeout); + + ret = WITHOUT_GVL_CALL(shutdown_without_gvl, &args); if (!ret) rb_raise(Error, "unable to shutdown container"); return self; } +struct snapshot_without_gvl_args +{ + struct container_data *data; + char *path; +}; + +static WITHOUT_GVL_RESULT_TYPE +snapshot_without_gvl(void* args_void) +{ + struct snapshot_without_gvl_args *args = (struct snapshot_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->snapshot(args->data->container, args->path)); +} + /* * call-seq: * container.snapshot(path = nil) @@ -1428,17 +1662,16 @@ static VALUE container_snapshot(int argc, VALUE *argv, VALUE self) { int ret; - char *path; char new_name[20]; - struct container_data *data; VALUE rb_path; + struct snapshot_without_gvl_args args; rb_scan_args(argc, argv, "01", &rb_path); - path = NIL_P(rb_path) ? NULL : StringValuePtr(rb_path); + args.path = NIL_P(rb_path) ? NULL : StringValuePtr(rb_path); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->snapshot(data->container, path); + ret = WITHOUT_GVL_CALL(snapshot_without_gvl, &args); if (ret < 0) rb_raise(Error, "unable to snapshot container"); @@ -1449,6 +1682,19 @@ container_snapshot(int argc, VALUE *argv, VALUE self) return rb_str_new2(new_name); } +struct snapshot_destroy_without_gvl_args +{ + struct container_data *data; + char *name; +}; + +static WITHOUT_GVL_RESULT_TYPE +snapshot_destroy_without_gvl(void *args_void) +{ + struct snapshot_destroy_without_gvl_args *args = (struct snapshot_destroy_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->snapshot_destroy(args->data->container, args->name)); +} + /* * call-seq: * container.snapshot_destroy(name) @@ -1459,20 +1705,32 @@ static VALUE container_snapshot_destroy(VALUE self, VALUE rb_name) { int ret; - char *name; - struct container_data *data; + struct snapshot_destroy_without_gvl_args args; - name = StringValuePtr(rb_name); + Data_Get_Struct(self, struct container_data, args.data); - Data_Get_Struct(self, struct container_data, data); + args.name = StringValuePtr(rb_name); - ret = data->container->snapshot_destroy(data->container, name); + ret = WITHOUT_GVL_CALL(snapshot_destroy_without_gvl, &args); if (!ret) rb_raise(Error, "unable to destroy snapshot"); return self; } +struct snapshot_list_without_gvl_args +{ + struct container_data *data; + struct lxc_snapshot *snapshots; +}; + +static WITHOUT_GVL_RESULT_TYPE +snapshot_list_without_gvl(void *args_void) +{ + struct snapshot_list_without_gvl_args *args = (struct snapshot_list_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->snapshot_list(args->data->container, &args->snapshots)); +} + /* * call-seq: * container.snapshot_list @@ -1483,30 +1741,43 @@ static VALUE container_snapshot_list(VALUE self) { int i, num_snapshots; - struct lxc_snapshot *snapshots; - struct container_data *data; VALUE rb_snapshots; + struct snapshot_list_without_gvl_args args; - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - num_snapshots = data->container->snapshot_list(data->container, &snapshots); + num_snapshots = WITHOUT_GVL_CALL(snapshot_list_without_gvl, &args); if (num_snapshots < 0) rb_raise(Error, "unable to list snapshots"); rb_snapshots = rb_ary_new2(num_snapshots); for (i = 0; i < num_snapshots; i++) { VALUE attrs = rb_ary_new2(4); - rb_ary_store(attrs, 0, rb_str_new2(snapshots[i].name)); - rb_ary_store(attrs, 1, rb_str_new2(snapshots[i].comment_pathname)); - rb_ary_store(attrs, 2, rb_str_new2(snapshots[i].timestamp)); - rb_ary_store(attrs, 3, rb_str_new2(snapshots[i].lxcpath)); - snapshots[i].free(&snapshots[i]); + rb_ary_store(attrs, 0, rb_str_new2(args.snapshots[i].name)); + rb_ary_store(attrs, 1, rb_str_new2(args.snapshots[i].comment_pathname)); + rb_ary_store(attrs, 2, rb_str_new2(args.snapshots[i].timestamp)); + rb_ary_store(attrs, 3, rb_str_new2(args.snapshots[i].lxcpath)); + args.snapshots[i].free(&args.snapshots[i]); rb_ary_store(rb_snapshots, i, attrs); } return rb_snapshots; } +struct snapshot_restore_without_gvl_args +{ + struct container_data *data; + char *name; + char *new_name; +}; + +static WITHOUT_GVL_RESULT_TYPE +snapshot_restore_without_gvl(void *args_void) +{ + struct snapshot_restore_without_gvl_args *args = (struct snapshot_restore_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->snapshot_restore(args->data->container, args->name, args->new_name)); +} + /* * call-seq: * container.snapshot_restore(name, new_name = nil) @@ -1517,23 +1788,42 @@ static VALUE container_snapshot_restore(int argc, VALUE *argv, VALUE self) { int ret; - char *name, *new_name; - struct container_data *data; + struct snapshot_restore_without_gvl_args args; VALUE rb_name, rb_new_name; rb_scan_args(argc, argv, "11", &rb_name, &rb_new_name); - name = StringValuePtr(rb_name); - new_name = NIL_P(rb_new_name) ? NULL : StringValuePtr(rb_new_name); + args.name = StringValuePtr(rb_name); + args.new_name = NIL_P(rb_new_name) ? NULL : StringValuePtr(rb_new_name); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->snapshot_restore(data->container, name, new_name); + ret = WITHOUT_GVL_CALL(snapshot_restore_without_gvl, &args); if (!ret) rb_raise(Error, "unable to restore snapshot"); return self; } + +struct start_without_gvl_args +{ + struct container_data *data; + int use_init; + int daemonize; + int close_fds; + char **args; +}; + +static WITHOUT_GVL_RESULT_TYPE +start_without_gvl(void *args_void) +{ + struct start_without_gvl_args *args = (struct start_without_gvl_args *)args_void; + struct lxc_container *container = args->data->container; + container->want_close_all_fds(container, args->close_fds); + container->want_daemonize(container, args->daemonize); + return WITHOUT_GVL_RESULT(container->start(container, args->use_init, args->args)); +} + /* * call-seq: * container.start(opts = {}) @@ -1548,41 +1838,38 @@ container_snapshot_restore(int argc, VALUE *argv, VALUE self) static VALUE container_start(int argc, VALUE *argv, VALUE self) { - int ret, use_init, daemonize, close_fds; - char **args; - struct container_data *data; + int ret; VALUE rb_use_init, rb_daemonize, rb_close_fds, rb_args, rb_opts; + struct start_without_gvl_args args; - use_init = 0; - daemonize = 1; - close_fds = 0; - args = NULL; + args.use_init = 0; + args.daemonize = 1; + args.close_fds = 0; + args.args = NULL; rb_args = Qnil; rb_scan_args(argc, argv, "01", &rb_opts); if (!NIL_P(rb_opts)) { Check_Type(rb_opts, T_HASH); rb_use_init = rb_hash_aref(rb_opts, SYMBOL("use_init")); - use_init = (rb_use_init != Qnil) && (rb_use_init != Qfalse); + args.use_init = (rb_use_init != Qnil) && (rb_use_init != Qfalse); rb_daemonize = rb_hash_aref(rb_opts, SYMBOL("daemonize")); - daemonize = (rb_daemonize != Qnil) && (rb_daemonize != Qfalse); + args.daemonize = (rb_daemonize != Qnil) && (rb_daemonize != Qfalse); rb_close_fds = rb_hash_aref(rb_opts, SYMBOL("close_fds")); - close_fds = (rb_close_fds != Qnil) && (rb_close_fds != Qfalse); + args.close_fds = (rb_close_fds != Qnil) && (rb_close_fds != Qfalse); rb_args = rb_hash_aref(rb_opts, SYMBOL("args")); - args = NIL_P(rb_args) ? NULL : ruby_to_c_string_array(rb_args); + args.args = NIL_P(rb_args) ? NULL : ruby_to_c_string_array(rb_args); } - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - data->container->want_close_all_fds(data->container, close_fds); - data->container->want_daemonize(data->container, daemonize); - ret = data->container->start(data->container, use_init, args); + ret = WITHOUT_GVL_CALL(start_without_gvl, &args); if (!NIL_P(rb_args)) - free_c_string_array(args); + free_c_string_array(args.args); if (!ret) rb_raise(Error, "unable to start container"); @@ -1590,6 +1877,13 @@ container_start(int argc, VALUE *argv, VALUE self) return self; } +static WITHOUT_GVL_RESULT_TYPE +stop_without_gvl(void *data_void) +{ + struct container_data *data = (struct container_data *)data_void; + return WITHOUT_GVL_RESULT(data->container->stop(data->container)); +} + /* * call-seq: * container.stop @@ -1604,13 +1898,20 @@ container_stop(VALUE self) Data_Get_Struct(self, struct container_data, data); - ret = data->container->stop(data->container); + ret = WITHOUT_GVL_CALL(stop_without_gvl, data); if (!ret) rb_raise(Error, "unable to stop container"); return self; } +static WITHOUT_GVL_RESULT_TYPE +unfreeze_without_gvl(void *data_void) +{ + struct container_data *data = (struct container_data *)data_void; + return WITHOUT_GVL_RESULT(data->container->unfreeze(data->container)); +} + /* * call-seq: * container.unfreeze @@ -1625,13 +1926,27 @@ container_unfreeze(VALUE self) Data_Get_Struct(self, struct container_data, data); - ret = data->container->unfreeze(data->container); + ret = WITHOUT_GVL_CALL(unfreeze_without_gvl, data); if (!ret) - rb_raise(Error, "unable to unfreeze container"); + rb_raise(Error, "unable to unfreeze container: #{lxc_strerror(ret)}"); return self; } +struct wait_without_gvl_args +{ + struct container_data *data; + int timeout; + char *state; +}; + +static WITHOUT_GVL_RESULT_TYPE +wait_without_gvl(void *args_void) +{ + struct wait_without_gvl_args *args = (struct wait_without_gvl_args *)args_void; + return WITHOUT_GVL_RESULT(args->data->container->wait(args->data->container, args->state, args->timeout)); +} + /* * call-seq: * container.wait(state, timeout = -1) @@ -1642,22 +1957,21 @@ container_unfreeze(VALUE self) static VALUE container_wait(int argc, VALUE *argv, VALUE self) { - int ret, timeout; - char *state; - struct container_data *data; + int ret; VALUE rb_state_str, rb_state, rb_timeout; + struct wait_without_gvl_args args; rb_scan_args(argc, argv, "11", &rb_state, &rb_timeout); rb_state_str = rb_funcall(rb_state, rb_intern("to_s"), 0); rb_state_str = rb_funcall(rb_state_str, rb_intern("upcase"), 0); - state = StringValuePtr(rb_state_str); + args.state = StringValuePtr(rb_state_str); - timeout = NIL_P(rb_timeout) ? -1 : NUM2INT(rb_timeout); + args.timeout = NIL_P(rb_timeout) ? -1 : NUM2INT(rb_timeout); - Data_Get_Struct(self, struct container_data, data); + Data_Get_Struct(self, struct container_data, args.data); - ret = data->container->wait(data->container, state, timeout); + ret = WITHOUT_GVL_CALL(wait_without_gvl, &args); if (!ret) rb_raise(Error, "error waiting for container"); @@ -1713,7 +2027,7 @@ Init_lxc(void) rb_define_method(Container, "load_config", container_load_config, -1); rb_define_method(Container, "reboot", container_reboot, 0); rb_define_method(Container, "remove_device_node", - container_remove_device_node, 0); + container_remove_device_node, -1); rb_define_method(Container, "rename", container_rename, 1); rb_define_method(Container, "running_config_item", container_running_config_item, 1); diff --git a/test/test_helpers.rb b/test/test_helpers.rb new file mode 100644 index 0000000..829145a --- /dev/null +++ b/test/test_helpers.rb @@ -0,0 +1,27 @@ +require 'lxc' +require 'test/unit/assertions' + +module TestHelpers + def assert_long_running_function_does_not_block_ruby(&block) + r, w = IO.pipe + begin + # Write something after a very short period, but only if Ruby isn't blocked! + t = Thread.new do + sleep(0.001) + # Sleep twice so that if the function is blocking and we wake up just + # after block.call(c) somehow, we will go back to sleep briefly and + # allow B to be written + sleep(0.001) + w.write('A') + end + # Call the function and see if Ruby gets blocked + block.call + w.write('B') + chars = r.read(2) + assert(chars == 'AB', "Expected thread to write before block finished, but it did not. Expected 'AB', got '#{chars}'") + ensure + r.close + w.close + end + end +end diff --git a/test/test_lxc_create_asynchronous.rb b/test/test_lxc_create_asynchronous.rb new file mode 100644 index 0000000..494a022 --- /dev/null +++ b/test/test_lxc_create_asynchronous.rb @@ -0,0 +1,27 @@ +$:.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib')) + +if RUBY_VERSION.to_f > 1.8 + require 'test/unit' + require 'lxc' + require 'test_helpers' + + class TestLXCCreateAsynchronous < Test::Unit::TestCase + include TestHelpers + + def setup + if Process::Sys::geteuid != 0 + raise 'This test must be ran as root' + end + @name = 'test_async_create' + container = LXC::Container.new(@name) + container.destroy if container.defined? + end + + def test_create_allows_ruby_to_continue + c = LXC::Container.new(@name) + assert_long_running_function_does_not_block_ruby do + c.create('ubuntu') + end + end + end +end diff --git a/test/test_lxc_created.rb b/test/test_lxc_created.rb index e48beb8..da30e16 100644 --- a/test/test_lxc_created.rb +++ b/test/test_lxc_created.rb @@ -6,11 +6,15 @@ require 'lxc' class TestLXCCreated < Test::Unit::TestCase def setup if Process::Sys::geteuid != 0 - raise 'This test must be ran as root' + raise 'This test must be run as root' end @name = 'test' @container = LXC::Container.new(@name) @container.create('ubuntu') unless @container.defined? + # Make sure the renamed_test container does not exist, for the rename test + @new_name = "renamed_#{@name}" + new_container = LXC::Container.new(@new_name) + new_container.destroy if new_container.defined? end def test_container_defined @@ -41,9 +45,9 @@ class TestLXCCreated < Test::Unit::TestCase end def test_container_rename - new_name = "renamed_#{@name}" - renamed = @container.rename(new_name) - assert_equal(new_name, renamed.name) + @container.stop if @container.running? + renamed = @container.rename(@new_name) + assert_equal(@new_name, renamed.name) rerenamed = renamed.rename(@name) assert_equal(@name, rerenamed.name) end diff --git a/test/test_lxc_long_running.rb b/test/test_lxc_long_running.rb new file mode 100644 index 0000000..adffeb4 --- /dev/null +++ b/test/test_lxc_long_running.rb @@ -0,0 +1,230 @@ +$:.unshift File.expand_path(File.join(File.dirname(__FILE__), 'lib')) + +require 'test/unit' +require 'lxc' +require 'test_helpers' + +if RUBY_VERSION.to_f > 1.8 + class TestLXCLongRunning < Test::Unit::TestCase + include TestHelpers + + def setup + if Process::Sys::geteuid != 0 + raise 'This test must be run as root' + end + @name = 'test' + @container = LXC::Container.new(@name) + @container.create('ubuntu') unless @container.defined? + @container.start unless @container.running? + @container.unfreeze + # Destroy leftover snapshots so we don't take up the whole disk + @container.snapshot_list.each do |snapshot, commentfile, timestamp, snapshotfile| + @container.snapshot_destroy(snapshot) + end + end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_add_device_node_allows_ruby_to_continue + # assert_long_running_function_does_not_block_ruby do + # @container.add_device_node('/dev/ttyS0') + # end + # end + + def test_attach_wait_true_allows_ruby_to_continue + assert_long_running_function_does_not_block_ruby do + @container.attach(:wait => true) do + sleep(2) + end + end + end + + def test_clone_allows_ruby_to_continue + # Ensure the "cloned" VM does not exist + cloned_name = "#{@name}_cloned" + cloned_to_destroy = LXC::Container.new(cloned_name) + cloned_to_destroy.destroy if cloned_to_destroy.defined? + + cloned = nil + @container.stop + begin + assert_long_running_function_does_not_block_ruby do + cloned = @container.clone(cloned_name) + end + ensure + @container.start + end + # Verify that it did something + assert(@container.name == @name, "original container name has changed from '#{@name}' to '#{@container.name}''") + assert(@container.running?, "original container is not running") + assert(cloned.name == cloned_name, "new container name should be '#{cloned_name}', is '#{cloned.name}'") + assert(cloned.init_pid != @container.init_pid, "cloned container init pid is the same as the original! Something ain't right.") + end + + # TODO Unsure how to test this without attaching to the actual tty! Must be a way to obtain a pipe or false tty of some sort. Manual run reveals it works, however. + # def test_console_allows_ruby_to_continue + # assert_long_running_function_does_not_block_ruby do + # @container.console + # end + # end + + def test_destroy_allows_ruby_to_continue + container = LXC::Container.new("test_destroy") + container.create('ubuntu') if !container.defined? + assert_long_running_function_does_not_block_ruby do + container.destroy + end + assert(!container.defined?, "Destroy did not destroy the container!") + end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_freeze_allows_ruby_to_continue + # begin + # assert_long_running_function_does_not_block_ruby do + # @container.freeze + # end + # ensure + # @container.unfreeze + # end + # end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_load_config_allows_ruby_to_continue + # File.open('/tmp/blah', 'w') do |file| + # file.write("\n\n\n\n") + # end + # begin + # container = LXC::Container.new('blahdeblah') + # assert_long_running_function_does_not_block_ruby do + # container.load_config('/tmp/blah') + # end + # ensure + # File.delete('/tmp/blah') + # end + # end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_reboot_allows_ruby_to_continue + # assert_long_running_function_does_not_block_ruby do + # @container.reboot + # end + # end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_remove_device_node_allows_ruby_to_continue + # assert_long_running_function_does_not_block_ruby do + # @container.remove_device_node('/dev/ttyS0') + # end + # end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_save_config_allows_ruby_to_continue + # File.unlink('/tmp/blah') if File.exist?('/tmp/blah') + # assert_long_running_function_does_not_block_ruby do + # @container.save_config('/tmp/blah') + # end + # assert(File.exist?('/tmp/blah'), "save_config did not save /tmp/blah!") + # end + + def test_shutdown_allows_ruby_to_continue + begin + assert_long_running_function_does_not_block_ruby do + @container.shutdown + end + ensure + @container.start if !@container.running? + end + end + + def test_snapshot_allows_ruby_to_continue + @container.stop + begin + assert_long_running_function_does_not_block_ruby do + @container.snapshot + end + ensure + @container.start + end + end + + def test_snapshot_destroy_allows_ruby_to_continue + @container.stop + begin + snapshot = @container.snapshot + assert_long_running_function_does_not_block_ruby do + @container.snapshot_destroy(snapshot) + end + ensure + @container.start + end + end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_snapshot_list_allows_ruby_to_continue + # @container.stop + # begin + # snapshot = @container.snapshot + # assert_long_running_function_does_not_block_ruby do + # assert(@container.snapshot_list > 0, "Snapshot list was empty!" + # end + # ensure + # @container.start + # end + # end + + def test_snapshot_restore_allows_ruby_to_continue + @container.stop + begin + snapshot = @container.snapshot + assert_long_running_function_does_not_block_ruby do + @container.snapshot_restore(snapshot) + end + ensure + @container.start + end + end + + def test_start_allows_ruby_to_continue + @container.stop + assert_long_running_function_does_not_block_ruby do + @container.start + end + assert(@container.running?, "Start did not start container! State = #{@container.state}") + end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_stop_allows_ruby_to_continue + # begin + # assert_long_running_function_does_not_block_ruby do + # @container.stop + # end + # assert(!@container.running?, "Stop did not stop container! State = #{@container.state}") + # ensure + # @container.start if !@container.running? + # end + # end + + # TODO find a way to test unblockness for functions that don't run quite so long + # def test_unfreeze_allows_ruby_to_continue + # @container.freeze + # assert_long_running_function_does_not_block_ruby do + # @container.unfreeze + # end + # end + + def test_wait_allows_ruby_to_continue + t = Thread.new do + sleep(0.5) + @container.stop + end + begin + assert_long_running_function_does_not_block_ruby do + @container.wait(:stopped, 2) + end + assert(@container.state == :stopped, "Container never stopped! State is now #{@container.state.inspect}") + ensure + t.join + @container.start if !@container.running? + end + end + end +end