diff --git a/ext/etc/etc.c b/ext/etc/etc.c index fcbd1af..d16aa57 100644 --- a/ext/etc/etc.c +++ b/ext/etc/etc.c @@ -10,6 +10,7 @@ #include "ruby.h" #include "ruby/encoding.h" #include "ruby/io.h" +#include "ruby/thread.h" #include #ifdef HAVE_UNISTD_H @@ -58,6 +59,16 @@ RUBY_EXTERN char *getlogin(void); #define RUBY_ETC_VERSION "1.4.3" +# if defined(HAVE_GETPWNAM_R) || defined(HAVE_GETPWUID_R) +# define GETPW_R_SIZE_DEFAULT 0x1000 +# define GETPW_R_SIZE_LIMIT 0x10000 +# if defined(_SC_GETPW_R_SIZE_MAX) +# define GETPW_R_SIZE_INIT sysconf(_SC_GETPW_R_SIZE_MAX) +# else +# define GETPW_R_SIZE_INIT GETPW_R_SIZE_DEFAULT +# endif +# endif + #ifdef HAVE_RB_DEPRECATE_CONSTANT void rb_deprecate_constant(VALUE mod, const char *name); #else @@ -94,6 +105,19 @@ atomic_exchange(volatile rb_atomic_t *var, rb_atomic_t newval) } #endif +#if defined(TEST_THREAD_SAFETY) +static void * +WITHOUT_GVL(void *(*func)(void *), void *arg) +{ + void *p = rb_thread_call_without_gvl(func, arg, RUBY_UBF_IO, 0); + rb_thread_schedule(); + return p; +} +#else +# define WITHOUT_GVL(func, arg) rb_thread_call_without_gvl((func), (arg), RUBY_UBF_IO, 0) +#endif +#define WITHOUT_GVL_INT(func, arg) (int)(VALUE)WITHOUT_GVL((func), (arg)) + /* call-seq: * getlogin -> String * @@ -157,6 +181,19 @@ safe_setup_filesystem_str(const char *str) #endif #ifdef HAVE_GETPWENT +static void * +nogvl_setpwent(void *_) +{ + setpwent(); + return NULL; +} + +static void * +nogvl_endpwent(void *_) +{ + endpwent(); + return NULL; +} # ifdef __APPLE__ # define PW_TIME2VAL(t) INT2NUM((int)(t)) # else @@ -200,6 +237,56 @@ setup_passwd(struct passwd *pwd) 0 /*dummy*/ ); } + +#if HAVE_GETPWUID_R +struct getpwuid_r_args { + uid_t uid; + char *buf; + size_t bufsize; + struct passwd *result; + struct passwd pwstore; +}; + +# define GETPWUID_R_ARGS(uid_, buf_, bufsize_) (struct getpwuid_r_args) \ + {.uid = uid_, .buf = buf_, .bufsize = bufsize_, .result = NULL} + +static void * +nogvl_getpwuid_r(void *args) +{ + struct getpwuid_r_args *arg = args; + return (void *)(VALUE)getpwuid_r(arg->uid, &arg->pwstore, arg->buf, arg->bufsize, &arg->result); +} +#endif + +# ifdef HAVE_GETPWNAM_R +struct getpwnam_r_args { + char *login; + char *buf; + size_t bufsize; + struct passwd *result; + struct passwd pwstore; +}; + +# define GETPWNAM_R_ARGS(login_, buf_, bufsize_) (struct getpwnam_r_args) \ + {.login = login_, .buf = buf_, .bufsize = bufsize_, .result = NULL} + +static void * +nogvl_getpwnam_r(void *args) +{ + struct getpwnam_r_args *arg = args; + return (void *)(VALUE)getpwnam_r(arg->login, &arg->pwstore, arg->buf, arg->bufsize, &arg->result); +} +# endif + +static VALUE +setup_passwd_getpwent(void) +{ + struct passwd *pw; + if ((pw = getpwent()) != 0) + return setup_passwd(pw); + else + return Qnil; +} #endif /* call-seq: @@ -226,7 +313,16 @@ etc_getpwuid(int argc, VALUE *argv, VALUE obj) #if defined(HAVE_GETPWENT) VALUE id; rb_uid_t uid; - struct passwd *pwd; +# ifdef HAVE_GETPWUID_R + char *bufid; + long bufsizeid = GETPW_R_SIZE_INIT; /* maybe -1 */ + VALUE getpwid_tmp; + struct getpwuid_r_args args; + int eid; + VALUE result; +# else + struct passwd *pwd = getpwuid((uid_t)uid); +# endif if (rb_scan_args(argc, argv, "01", &id) == 1) { uid = NUM2UIDT(id); @@ -234,9 +330,51 @@ etc_getpwuid(int argc, VALUE *argv, VALUE obj) else { uid = getuid(); } - pwd = getpwuid(uid); + +# ifdef HAVE_GETPWUID_R + if (bufsizeid < 0) + bufsizeid = GETPW_R_SIZE_DEFAULT; + + getpwid_tmp = rb_str_tmp_new(bufsizeid); + + bufid = RSTRING_PTR(getpwid_tmp); + bufsizeid = rb_str_capacity(getpwid_tmp); + rb_str_set_len(getpwid_tmp, bufsizeid); + args = GETPWUID_R_ARGS(uid, bufid, bufsizeid); + + errno = 0; + while ((eid = WITHOUT_GVL_INT(nogvl_getpwuid_r, &args)) != 0) { + + if (eid == ENOENT || eid== ESRCH || eid == EBADF || eid == EPERM) { + /* not found; non-errors */ + rb_str_resize(getpwid_tmp, 0); + return Qnil; + } + + if (eid != ERANGE || args.bufsize >= GETPW_R_SIZE_LIMIT) { + rb_str_resize(getpwid_tmp, 0); + rb_syserr_fail(eid, "getpwuid_r"); + } + + rb_str_modify_expand(getpwid_tmp, args.bufsize); + args.buf = RSTRING_PTR(getpwid_tmp); + args.bufsize = (size_t)rb_str_capacity(getpwid_tmp); + } + + if (args.result == NULL) { + /* no record in the password database for the uid */ + rb_str_resize(getpwid_tmp, 0); + return Qnil; + } + + /* found it */ + result = setup_passwd(args.result); + rb_str_resize(getpwid_tmp, 0); + return result; +# else if (pwd == 0) rb_raise(rb_eArgError, "can't find user for %d", (int)uid); return setup_passwd(pwd); +# endif #else return Qnil; #endif @@ -261,12 +399,61 @@ static VALUE etc_getpwnam(VALUE obj, VALUE nam) { #ifdef HAVE_GETPWENT - struct passwd *pwd; - const char *p = StringValueCStr(nam); +# ifdef HAVE_GETPWNAM_R + char *login = RSTRING_PTR(nam); + char *bufnm; + long bufsizenm = GETPW_R_SIZE_INIT; /* maybe -1 */ + VALUE getpwnm_tmp; + struct getpwnam_r_args args; + int enm; + VALUE result; + + if (bufsizenm < 0) + bufsizenm = GETPW_R_SIZE_DEFAULT; - pwd = getpwnam(p); + getpwnm_tmp = rb_str_tmp_new(bufsizenm); + bufnm = RSTRING_PTR(getpwnm_tmp); + bufsizenm = rb_str_capacity(getpwnm_tmp); + rb_str_set_len(getpwnm_tmp, bufsizenm); + args = GETPWNAM_R_ARGS(login, bufnm, bufsizenm); + + errno = 0; + while ((enm = WITHOUT_GVL_INT(nogvl_getpwnam_r, &args)) != 0) { + + if (enm == ENOENT || enm== ESRCH || enm == EBADF || enm == EPERM) { + /* not found; non-errors */ + rb_str_resize(getpwnm_tmp, 0); + return Qnil; + } + + if (enm != ERANGE || args.bufsize >= GETPW_R_SIZE_LIMIT) { + rb_str_resize(getpwnm_tmp, 0); + rb_syserr_fail(enm, "getpwnam_r"); + } + + rb_str_modify_expand(getpwnm_tmp, bufsizenm); + args.buf = RSTRING_PTR(getpwnm_tmp); + args.bufsize = (size_t)rb_str_capacity(getpwnm_tmp); + } + + if (args.result == NULL) { + /* no record in the password database for the login name */ + rb_str_resize(getpwnm_tmp, 0); + return Qnil; + } + + /* found it */ + result = setup_passwd(args.result); + rb_str_resize(getpwnm_tmp, 0); + RB_GC_GUARD(nam); + return result; +# else + const char *p = StringValueCStr(nam); + struct passwd *pwd = getpwnam(p); if (pwd == 0) rb_raise(rb_eArgError, "can't find user for %"PRIsVALUE, nam); + RB_GC_GUARD(nam); return setup_passwd(pwd); +# endif #else return Qnil; #endif @@ -277,7 +464,7 @@ static rb_atomic_t passwd_blocking; static VALUE passwd_ensure(VALUE _) { - endpwent(); + WITHOUT_GVL(nogvl_endpwent, NULL); if (RUBY_ATOMIC_EXCHANGE(passwd_blocking, 0) != 1) { rb_raise(rb_eRuntimeError, "unexpected passwd_blocking"); } @@ -287,11 +474,11 @@ passwd_ensure(VALUE _) static VALUE passwd_iterate(VALUE _) { - struct passwd *pw; + VALUE pw; - setpwent(); - while ((pw = getpwent()) != 0) { - rb_yield(setup_passwd(pw)); + WITHOUT_GVL(nogvl_setpwent, NULL); + while ((pw = setup_passwd_getpwent()) != Qnil) { + rb_yield(pw); } return Qnil; } @@ -330,16 +517,13 @@ static VALUE etc_passwd(VALUE obj) { #ifdef HAVE_GETPWENT - struct passwd *pw; - if (rb_block_given_p()) { each_passwd(); } - else if ((pw = getpwent()) != 0) { - return setup_passwd(pw); - } -#endif + return setup_passwd_getpwent(); +#else return Qnil; +#endif } /* call-seq: @@ -387,7 +571,7 @@ static VALUE etc_setpwent(VALUE obj) { #ifdef HAVE_GETPWENT - setpwent(); + WITHOUT_GVL(nogvl_setpwent, NULL); #endif return Qnil; } @@ -402,7 +586,7 @@ static VALUE etc_endpwent(VALUE obj) { #ifdef HAVE_GETPWENT - endpwent(); + WITHOUT_GVL(nogvl_endpwent, NULL); #endif return Qnil; } @@ -425,16 +609,76 @@ static VALUE etc_getpwent(VALUE obj) { #ifdef HAVE_GETPWENT - struct passwd *pw; - - if ((pw = getpwent()) != 0) { - return setup_passwd(pw); - } -#endif + return setup_passwd_getpwent(); +#else return Qnil; +#endif } #ifdef HAVE_GETGRENT + +# if (defined(HAVE_GETGRNAM_R) || defined(HAVE_GETGRGID_R)) && defined(_SC_GETGR_R_SIZE_MAX) +# define GETGR_R_SIZE_INIT sysconf(_SC_GETGR_R_SIZE_MAX) +# define GETGR_R_SIZE_DEFAULT 0x1000 +# define GETGR_R_SIZE_LIMIT 0x10000 +# else +# define GETGR_R_SIZE_INIT -1 +# endif + +#if HAVE_GETGRGID_R +struct getgrgid_r_args { + gid_t gid; + char *buf; + size_t bufsize; + struct group *result; + struct group grp; +}; + +# define GETGRGID_R_ARGS(gid_, buf_, bufsize_) (struct getgrgid_r_args) \ + {.gid = gid_, .buf = buf_, .bufsize = bufsize_, .result = NULL} + +static void * +nogvl_getgrgid_r(void *args) +{ + struct getgrgid_r_args *arg = args; + return (void *)(VALUE)getgrgid_r(arg->gid, &arg->grp, arg->buf, arg->bufsize, &arg->result); +} +#endif + +# ifdef HAVE_GETGRNAM_R +struct getgrnam_r_args { + const char *login; + char *buf; + size_t bufsize; + struct group *result; + struct group grp; +}; + +# define GETGRNAM_R_ARGS(login_, buf_, bufsize_) (struct getgrnam_r_args) \ + {.login = login_, .buf = buf_, .bufsize = bufsize_, .result = NULL} + +static void * +nogvl_getgrnam_r(void *args) +{ + struct getgrnam_r_args *arg = args; + return (void *)(VALUE)getgrnam_r(arg->login, &arg->grp, arg->buf, arg->bufsize, &arg->result); +} +# endif + +static void * +nogvl_setgrent(void *_) +{ + setgrent(); + return NULL; +} + +static void * +nogvl_endgrent(void *_) +{ + endgrent(); + return NULL; +} + static VALUE setup_group(struct group *grp) { @@ -455,6 +699,16 @@ setup_group(struct group *grp) GIDT2NUM(grp->gr_gid), mem); } + +static VALUE +setup_group_getgrent(void) +{ + struct group *grp; + if ((grp = getgrent()) != 0) + return setup_group(grp); + else + return Qnil; +} #endif /* call-seq: @@ -480,6 +734,14 @@ etc_getgrgid(int argc, VALUE *argv, VALUE obj) VALUE id; gid_t gid; struct group *grp; +# ifdef HAVE_GETGRGID_R + char *getgr_buf; + long getgr_buf_len; + int e; + VALUE getgr_tmp; + struct getgrgid_r_args args; + VALUE group; +# endif if (rb_scan_args(argc, argv, "01", &id) == 1) { gid = NUM2GIDT(id); @@ -487,9 +749,38 @@ etc_getgrgid(int argc, VALUE *argv, VALUE obj) else { gid = getgid(); } +# ifdef HAVE_GETGRGID_R + getgr_buf_len = GETGR_R_SIZE_INIT; + if (getgr_buf_len < 0) getgr_buf_len = GETGR_R_SIZE_DEFAULT; + getgr_tmp = rb_str_tmp_new(getgr_buf_len); + getgr_buf = RSTRING_PTR(getgr_tmp); + getgr_buf_len = rb_str_capacity(getgr_tmp); + rb_str_set_len(getgr_tmp, getgr_buf_len); + errno = 0; + args = GETGRGID_R_ARGS(gid, getgr_buf, getgr_buf_len); + + while ((e = WITHOUT_GVL_INT(nogvl_getgrgid_r, &args)) != 0) { + if (e != ERANGE || args.bufsize >= GETGR_R_SIZE_LIMIT) { + rb_str_resize(getgr_tmp, 0); + rb_syserr_fail(e, "getgrnam_r"); + } + rb_str_modify_expand(getgr_tmp, args.bufsize); + args.buf = RSTRING_PTR(getgr_tmp); + args.bufsize = (size_t)rb_str_capacity(getgr_tmp); + } + grp = args.result; + if (grp == NULL) { + rb_str_resize(getgr_tmp, 0); + rb_raise(rb_eArgError, "can't find group for %d", (int)gid); + } + group = setup_group(args.result); + rb_str_resize(getgr_tmp, 0); + return group; +# else grp = getgrgid(gid); - if (grp == 0) rb_raise(rb_eArgError, "can't find group for %d", (int)gid); + if (grp == NULL) rb_raise(rb_eArgError, "can't find group for %d", (int)gid); return setup_group(grp); +# endif #else return Qnil; #endif @@ -515,12 +806,49 @@ static VALUE etc_getgrnam(VALUE obj, VALUE nam) { #ifdef HAVE_GETGRENT + const char *grpname = StringValueCStr(nam); struct group *grp; - const char *p = StringValueCStr(nam); - - grp = getgrnam(p); - if (grp == 0) rb_raise(rb_eArgError, "can't find group for %"PRIsVALUE, nam); +# ifdef HAVE_GETGRNAM_R + char *getgr_buf; + long getgr_buf_len; + int e; + VALUE getgr_tmp; + struct getgrnam_r_args args; + VALUE group; + + getgr_buf_len = GETGR_R_SIZE_INIT; + if (getgr_buf_len < 0) getgr_buf_len = GETGR_R_SIZE_DEFAULT; + getgr_tmp = rb_str_tmp_new(getgr_buf_len); + getgr_buf = RSTRING_PTR(getgr_tmp); + getgr_buf_len = rb_str_capacity(getgr_tmp); + rb_str_set_len(getgr_tmp, getgr_buf_len); + errno = 0; + args = GETGRNAM_R_ARGS(grpname, getgr_buf, getgr_buf_len); + + while ((e = WITHOUT_GVL_INT(nogvl_getgrnam_r, &args)) != 0) { + if (e != ERANGE || args.bufsize >= GETGR_R_SIZE_LIMIT) { + rb_str_resize(getgr_tmp, 0); + rb_syserr_fail(e, "getgrnam_r"); + } + rb_str_modify_expand(getgr_tmp, args.bufsize); + args.buf = RSTRING_PTR(getgr_tmp); + args.bufsize = (size_t)rb_str_capacity(getgr_tmp); + } + grp = args.result; + if (grp == NULL) { + rb_str_resize(getgr_tmp, 0); + rb_raise(rb_eArgError, "can't find group for %"PRIsVALUE, nam); + } + group = setup_group(args.result); + rb_str_resize(getgr_tmp, 0); + RB_GC_GUARD(nam); + return group; +# else + grp = getgrnam(grpnam); + if (grp == NULL) rb_raise(rb_eArgError, "can't find group for %"PRIsVALUE, nam); + RB_GC_GUARD(nam); return setup_group(grp); +# endif #else return Qnil; #endif @@ -531,7 +859,7 @@ static rb_atomic_t group_blocking; static VALUE group_ensure(VALUE _) { - endgrent(); + WITHOUT_GVL(nogvl_endgrent, NULL); if (RUBY_ATOMIC_EXCHANGE(group_blocking, 0) != 1) { rb_raise(rb_eRuntimeError, "unexpected group_blocking"); } @@ -541,11 +869,11 @@ group_ensure(VALUE _) static VALUE group_iterate(VALUE _) { - struct group *pw; + VALUE grp; - setgrent(); - while ((pw = getgrent()) != 0) { - rb_yield(setup_group(pw)); + WITHOUT_GVL(nogvl_setgrent, NULL); + while ((grp = setup_group_getgrent()) != Qnil) { + rb_yield(grp); } return Qnil; } @@ -584,16 +912,13 @@ static VALUE etc_group(VALUE obj) { #ifdef HAVE_GETGRENT - struct group *grp; - if (rb_block_given_p()) { each_group(); } - else if ((grp = getgrent()) != 0) { - return setup_group(grp); - } -#endif + return setup_group_getgrent(); +#else return Qnil; +#endif } #ifdef HAVE_GETGRENT @@ -639,7 +964,7 @@ static VALUE etc_setgrent(VALUE obj) { #ifdef HAVE_GETGRENT - setgrent(); + WITHOUT_GVL(nogvl_setgrent, NULL); #endif return Qnil; } @@ -654,7 +979,7 @@ static VALUE etc_endgrent(VALUE obj) { #ifdef HAVE_GETGRENT - endgrent(); + WITHOUT_GVL(nogvl_endgrent, NULL); #endif return Qnil; } @@ -676,13 +1001,10 @@ static VALUE etc_getgrent(VALUE obj) { #ifdef HAVE_GETGRENT - struct group *gr; - - if ((gr = getgrent()) != 0) { - return setup_group(gr); - } -#endif + return setup_group_getgrent(); +#else return Qnil; +#endif } #define numberof(array) (sizeof(array) / sizeof(*(array))) diff --git a/ext/etc/extconf.rb b/ext/etc/extconf.rb index 2e28d58..23491bb 100644 --- a/ext/etc/extconf.rb +++ b/ext/etc/extconf.rb @@ -12,6 +12,10 @@ have_func("getlogin") have_func("getpwent") have_func("getgrent") +have_func("getpwuid_r") +have_func("getpwnam_r") +have_func("getgrgid_r") +have_func("getgrnam_r") if (sysconfdir = RbConfig::CONFIG["sysconfdir"] and !RbConfig.expand(sysconfdir.dup, "prefix"=>"", "DESTDIR"=>"").empty?) $defs.push("-DSYSCONFDIR=#{Shellwords.escape(sysconfdir.dump)}") diff --git a/test/etc/test_etc.rb b/test/etc/test_etc.rb index 2eddcf4..94f9571 100644 --- a/test/etc/test_etc.rb +++ b/test/etc/test_etc.rb @@ -3,6 +3,8 @@ require "etc" class TestEtc < Test::Unit::TestCase + MAX_THREADS = 20 + def test_getlogin s = Etc.getlogin return if s == nil @@ -46,6 +48,21 @@ def test_getpwuid end end unless RUBY_PLATFORM.include?("android") + def test_getpwuid_thread_safety + uids = [] + Etc.passwd {|s| uids << s.uid} + uids.uniq! + return unless uids = uids[-MAX_THREADS..-1] + + uids.map do |uid| + Thread.new do + 1000.times do + assert_equal uid, Etc.getpwuid(uid).uid + end + end + end.map(&:join) + end + def test_getpwnam passwd = {} Etc.passwd do |s| @@ -56,6 +73,24 @@ def test_getpwnam end end unless RUBY_PLATFORM.include?("android") + def test_getpwnam_thread_safety + passwd = {} + Etc.passwd do |s| + (passwd[s.name] ||= []) << s.uid unless /\A\+/ =~ s.name + end + passwd = passwd.to_a.reject{|name, uids| uids.length > 1} + return unless passwd = passwd[-MAX_THREADS..-1] + + passwd.map do |name, uids| + uid = uids.first + Thread.new do + 1000.times do + assert_equal uid, Etc.getpwnam(name).uid + end + end + end.map(&:join) + end + def test_passwd_with_low_level_api a = [] Etc.passwd {|s| a << s } @@ -96,6 +131,21 @@ def test_getgrgid end end + def test_getgrgid_thread_safety + gids = [] + Etc.group {|g| gids << g.gid} + gids.uniq! + return unless gids = gids[-MAX_THREADS..-1] + + gids.map do |gid| + Thread.new do + 1000.times do + assert_equal gid, Etc.getgrgid(gid).gid + end + end + end.map(&:join) + end + def test_getgrnam groups = Hash.new {[]} Etc.group do |s| @@ -106,6 +156,24 @@ def test_getgrnam end end + def test_getgrnam_thread_safety + groups = {} + Etc.group do |s| + (groups[s.name] ||= []) << s.gid unless /\A\+/ =~ s.name + end + groups = groups.to_a.reject{|name, gids| gids.length > 1} + return unless groups = groups[-MAX_THREADS..-1] + + groups.map do |name, gids| + gid = gids.first + Thread.new do + 1000.times do + assert_equal gid, Etc.getgrnam(name).gid + end + end + end.map(&:join) + end + def test_group_with_low_level_api a = [] Etc.group {|s| a << s }