[ruby-dev:15205] Re: strange behavior about PTY.spawn
From:
akira yamada / やまだあきら <akira@...>
Date:
2001-11-17 18:22:43 UTC
List:
ruby-dev #15205
Fri, 16 Nov 2001 14:20:26 +0900 頃の
Mail-Count: 15192
Subject: [ruby-dev:15192] Re: strange behavior about PTY.spawn
についてのお話にて Yukihiro Matsumoto さん曰く…
(Y == matz@ruby-lang.org (Yukihiro Matsumoto) さん)
In article 15192, <1005887777.341310.4251.nullmailer@ev.netlab.jp>
Y> 手元ではこういう風にしてます。確信がないのでコミットできませ
Y> んけど。
いろいろいじっていたらプロセスがPTY.spawn{}の範囲をこえて
生きのこってしまうことがあるようでした.
この点は仕様としてどうあるべきなのかよくわからないのですが,
ブロック付きの場合にはそのブロックとともに
どうにかなってほしいなあというのがわたしの考えです.
あと, どういう形でプロセスが終了したのかを知りたいことも
少なくないと思いましたので例外の中にその情報を入れてはどうか? と思い,
まつもとさんのパッチを適用した上でさらにいじってみました.
以下パッチです(まつもとさんのパッチも含んでしまっています):
Index: pty.c
===================================================================
RCS file: /home/akira/cvs/ruby-src/cvs/ruby/ext/pty/pty.c,v
retrieving revision 1.9
diff -u -r1.9 pty.c
--- pty.c 13 Nov 2001 08:14:21 -0000 1.9
+++ pty.c 17 Nov 2001 17:57:41 -0000
@@ -106,65 +106,136 @@
# endif /* HAVE_SETREUID */
#endif /* NO_SETEUID */
-struct pty_info {
- int fd;
- pid_t child_pid;
-};
+static VALUE eChildExited;
-static void
-pty_raise(cpid)
- int cpid;
+static VALUE
+echild_exited_code(self)
+ VALUE self;
{
- char buf[1024];
+ return rb_ivar_get(self, rb_intern("@exit_code"));
+}
- snprintf(buf, sizeof(buf),
- "eval %%Q{Thread.main.raise 'pty - stopped: %d'}, nil, \"%s\", %d",
- cpid, ruby_sourcefile, ruby_sourceline);
- rb_eval_string(buf);
+static VALUE
+echild_exited_signal(self)
+ VALUE self;
+{
+ return rb_ivar_get(self, rb_intern("@received_signal"));
}
static VALUE
-pty_syswait(pid)
- int pid;
+echild_exited_stopped(self)
+ VALUE self;
{
- int cpid, status;
+ return rb_ivar_get(self, rb_intern("@stopped"));
+}
- cpid = rb_waitpid(pid, &status, WUNTRACED);
+struct pty_info {
+ int fd;
+ pid_t child_pid;
+ VALUE thread;
+};
- printf("PTY command (%d) finished (%d:%d)\n", pid, cpid, status);
- if (cpid == 0 || cpid == -1)
- return Qnil;
+static void
+pty_raise(thread, info, status)
+ VALUE thread;
+ struct pty_info *info;
+ int status;
+{
+ char buf[1024];
+ struct child_info *cinfo;
+ NEWOBJ(e, struct RObject);
+ OBJSETUP(e, eChildExited, T_OBJECT);
+
+ if (WIFEXITED(status)) {
+ rb_ivar_set((VALUE)e, rb_intern("@exit_code"),
+ INT2FIX(WEXITSTATUS(status)));
+ snprintf(buf, sizeof(buf), "pty - exited (%d): %d",
+ WEXITSTATUS(status), info->child_pid);
+ }
+ else {
+ rb_ivar_set((VALUE)e, rb_intern("@exit_code"), Qnil);
+ }
+
+ if (WIFSIGNALED(status)) {
+ rb_ivar_set((VALUE)e, rb_intern("@received_signal"),
+ INT2FIX(WTERMSIG(status)));
+ snprintf(buf, sizeof(buf), "pty - received signal (%d): %d",
+ WTERMSIG(status), info->child_pid);
+ }
+ else {
+ rb_ivar_set((VALUE)e, rb_intern("@received_signal"), Qnil);
+ }
#ifdef IF_STOPPED
if (IF_STOPPED(status)) { /* suspend */
- pty_raise(cpid);
+ snprintf(buf, sizeof(buf), "pty - stopped: %d", info->child_pid);
+ rb_ivar_set((VALUE)e, rb_intern("@stopped"), Qtrue);
}
#else
#ifdef WIFSTOPPED
if (WIFSTOPPED(status)) { /* suspend */
- pty_raise(cpid);
+ snprintf(buf, sizeof(buf), "pty - stopped: %d", info->child_pid);
+ rb_ivar_set((VALUE)e, rb_intern("@stopped"), Qtrue);
}
#else
---->> Either IF_STOPPED or WIFSTOPPED is needed <<----
#endif /* WIFSTOPPED */
#endif /* IF_STOPPED */
+ else {
+ rb_ivar_set((VALUE)e, rb_intern("@stopped"), Qfalse);
+ }
+
+ rb_funcall(thread, rb_intern("raise"), 2, (VALUE)e, rb_str_new2(buf));
+}
+
+static VALUE
+pty_syswait(info)
+ struct pty_info *info;
+{
+ int cpid, status;
+
+ cpid = rb_waitpid(info->child_pid, &status, WUNTRACED);
+ printf("cpid: %d (%d)\n", cpid, status);
- return Qnil;
+ if (cpid == 0 || cpid == -1)
+ return Qnil;
+
+ pty_raise(info->thread, info, status);
}
static void getDevice _((int*, int*));
+struct exec_info {
+ int argc;
+ VALUE *argv;
+};
+
+static VALUE
+pty_exec(arg)
+ struct exec_info *arg;
+{
+ int argc;
+ VALUE *argv;
+ rb_f_exec(arg->argc, arg->argv);
+}
+
static void
-establishShell(shellname, info)
- char *shellname;
+establishShell(argc, argv, info)
+ int argc;
+ VALUE *argv;
struct pty_info *info;
{
static int i,j,master,slave,currentPid;
char *p,*getenv();
struct passwd *pwent;
RETSIGTYPE chld_changed();
-
- if (shellname[0] == '\0') {
+ VALUE v;
+ struct exec_info arg;
+ int status;
+
+ if (argc == 0) {
+ char *shellname;
+
if ((p = getenv("SHELL")) != NULL) {
shellname = p;
}
@@ -175,17 +246,20 @@
else
shellname = "/bin/sh";
}
+ v = rb_str_new2(shellname);
+ argc = 1;
+ argv = &v;
}
getDevice(&master,&slave);
currentPid = getpid();
- if((i = vfork()) < 0) {
+ if((i = fork()) < 0) {
rb_sys_fail("fork failed");
}
if(i == 0) { /* child */
- int argc;
- char *argv[1024];
+ /* int argc;
+ char *argv[1024]; */
currentPid = getpid();
/*
@@ -232,23 +306,13 @@
dup2(slave,1);
dup2(slave,2);
close(slave);
-
#if defined(HAVE_SETEUID) || defined(HAVE_SETREUID) || defined(HAVE_SETRESUID)
seteuid(getuid());
#endif
- argc = 0;
- for (i = 0; shellname[i];) {
- while (isspace(shellname[i])) i++;
- for (j = i; shellname[j] && !isspace(shellname[j]); j++);
- argv[argc] = (char*)xmalloc(j-i+1);
- strncpy(argv[argc],&shellname[i],j-i);
- argv[argc][j-i] = 0;
- i = j;
- argc++;
- }
- argv[argc] = NULL;
- execvp(argv[0],argv);
+ arg.argc = argc;
+ arg.argv = argv;
+ rb_protect(pty_exec, (VALUE)&arg, &status);
sleep(1);
_exit(1);
}
@@ -359,12 +423,38 @@
chown(SlaveName, 0, 0);
}
+static VALUE
+pty_kill_child(thinfo)
+ VALUE thinfo;
+{
+ VALUE th = RARRAY(thinfo)->ptr[0];
+ pid_t pid = RARRAY(thinfo)->ptr[1];
+
+ if (rb_funcall(th, rb_intern("alive?"), 0, 0) == Qtrue &&
+ kill(pid, 0) == 0) {
+ rb_thread_schedule();
+ if (kill(pid, SIGTERM) == 0) {
+ rb_thread_schedule();
+ if (kill(pid, 0) == 0) {
+ kill(pid, SIGINT);
+ rb_thread_schedule();
+ if (kill(pid, 0) == 0)
+ kill(pid, SIGKILL);
+ }
+ }
+ }
+ rb_funcall(th, rb_intern("value"), 0, 0);
+ return Qnil;
+}
+
/* ruby function: getpty */
static VALUE
-pty_getpty(self, command)
- VALUE self, command;
+pty_getpty(argc, argv, self)
+ int argc;
+ VALUE *argv;
+ VALUE self;
{
- VALUE res, th;
+ VALUE res, th, thinfo;
struct pty_info info;
OpenFile *wfptr,*rfptr;
VALUE rport = rb_obj_alloc(rb_cFile);
@@ -373,31 +463,33 @@
MakeOpenFile(rport, rfptr);
MakeOpenFile(wport, wfptr);
- if (TYPE(command) == T_ARRAY)
- command = rb_ary_join(command,rb_str_new2(" "));
- Check_SafeStr(command);
-
- establishShell(RSTRING(command)->ptr,&info);
+ establishShell(argc, argv, &info);
rfptr->mode = rb_io_mode_flags("r");
rfptr->f = fdopen(info.fd, "r");
- rfptr->path = strdup(RSTRING(command)->ptr);
+ rfptr->path = 0; /*strdup(RSTRING(command)->ptr); */
wfptr->mode = rb_io_mode_flags("w");
wfptr->f = fdopen(dup(info.fd), "w");
- wfptr->path = strdup(RSTRING(command)->ptr);
+ wfptr->path = 0; /* strdup(RSTRING(command)->ptr); */
res = rb_ary_new2(3);
rb_ary_store(res,0,(VALUE)rport);
rb_ary_store(res,1,(VALUE)wport);
rb_ary_store(res,2,INT2FIX(info.child_pid));
- printf("start watching PTY command (%d)\n", info.child_pid);
- th = rb_thread_create(pty_syswait, (void*)info.child_pid);
+ info.thread = rb_thread_current();
+ printf("thread %p\n", info.thread);
+ th = rb_thread_create(pty_syswait, (void*)&info);
+
+ thinfo = rb_ary_new2(2);
+ rb_ary_store(thinfo,0,(VALUE)th);
+ rb_ary_store(thinfo,1,(VALUE)info.child_pid);
+
if (rb_block_given_p()) {
- res = rb_yield((VALUE)res);
- rb_funcall(th, rb_intern("kill"), 0, 0);
- return res;
+ rb_ensure(rb_yield, (VALUE)res,
+ pty_kill_child, (VALUE)thinfo);
+ return Qnil;
}
else {
return res;
@@ -429,8 +521,13 @@
Init_pty()
{
cPTY = rb_define_module("PTY");
- rb_define_module_function(cPTY,"getpty",pty_getpty,1);
- rb_define_module_function(cPTY,"spawn",pty_getpty,1);
+ rb_define_module_function(cPTY,"getpty",pty_getpty,-1);
+ rb_define_module_function(cPTY,"spawn",pty_getpty,-1);
rb_define_module_function(cPTY,"protect_signal",pty_protect,0);
rb_define_module_function(cPTY,"reset_signal",pty_reset_signal,0);
+
+ eChildExited = rb_define_class_under(cPTY,"ChildExited",rb_eRuntimeError);
+ rb_define_method(eChildExited,"exit_code",echild_exited_code,0);
+ rb_define_method(eChildExited,"received_signal",echild_exited_signal,0);
+ rb_define_method(eChildExited,"stopped?",echild_exited_stopped,0);
}
ホントに簡単なものですがテストも作ってみました.
require 'runit/testcase'
require 'runit/cui/testrunner'
require 'pty'
class TestPTY < RUNIT::TestCase
def setup
@s = Signal::list
end
def test_STOP
pid = nil
e = assert_exception(PTY::ChildExited) {
PTY.spawn('/bin/sh') {|rd,wr,pid|
assert_equal(1, Process.kill(@s['STOP'], pid))
sleep 1
}
}
assert(pid != nil)
assert_equal(1, Process.kill(0, pid))
assert_equal(nil, e.exit_code)
assert_equal(nil, e.received_signal)
assert_equal(true, e.stopped?)
end
def test_KILL
pid = nil
e = assert_exception(PTY::ChildExited) {
PTY.spawn('/bin/sh') {|rd,wr,pid|
assert_equal(1, Process.kill(@s['KILL'], pid))
sleep 1
}
}
assert(pid != nil)
assert_exception(Errno::ESRCH) {Process.kill(0, pid)}
assert_equal(nil, e.exit_code)
assert_equal(@s['KILL'], e.received_signal)
assert_equal(false, e.stopped?)
end
def test_exit1
pid = nil
e = assert_exception(PTY::ChildExited) {
PTY.spawn('/bin/sh'){|rd,wr,pid|
wr.puts 'echo 123'
wr.puts 'exit'
loop {rd.sysread(1024)} rescue IOError
}
}
assert_exception(Errno::ESRCH) {Process.kill(0, pid)}
assert_equal(0, e.exit_code)
assert_equal(nil, e.received_signal)
assert_equal(false, e.stopped?)
end
def test_exit2
pid = nil
e = assert_exception(PTY::ChildExited) {
PTY.spawn('sleep 3') {|rd,wr,pid|
assert_equal(1, Process.kill(0, pid))
sleep
}
}
assert_exception(Errno::ESRCH) {Process.kill(0, pid)}
assert_equal(0, e.exit_code)
assert_equal(nil, e.received_signal)
assert_equal(false, e.stopped?)
end
def test_notexist
pid = nil
e = assert_exception(PTY::ChildExited) {
PTY.spawn('/not exist') {|rd,wr,pid|
sleep 3
assert_fail
}
}
assert_equal(1, e.exit_code)
assert_equal(nil, e.received_signal)
assert_equal(false, e.stopped?)
end
def test_thSTOP
th = Thread.new {test_STOP}
loop {sleep 0.5; break unless th.alive?}
end
def test_thKILL
th = Thread.new {test_KILL}
loop {sleep 0.5; break unless th.alive?}
end
def test_thexit1
th = Thread.new {test_exit1}
loop {sleep 0.5; break unless th.alive?}
end
def test_thexit2
th = Thread.new {test_exit2}
loop {sleep 0.5; break unless th.alive?}
end
def test_thnotexist
th = Thread.new {test_notexist}
loop {sleep 0.5; break unless th.alive?}
end
end
if $0 == __FILE__
if ARGV.size == 0
suite = TestPTY.suite
else
suite = RUNIT::TestSuite.new
ARGV.each do |testmethod|
suite.add(TestPTY.new(testmethod))
end
end
RUNIT::CUI::TestRunner.run(suite)
end
微妙な点もまだ残っていそうな気がしますが,
どんなものでしょうか?
--
やまだ あきら <URL:http://arika.org/>
(akira@arika.org, akira@ruby-lang.org or akira@linux.or.jp)