process_tail 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8de878e91fb0d39a77238a19bcb567e6257ed449
4
- data.tar.gz: 9b4353b4c9e7cd4ed6ee44cdc9796310448912a3
3
+ metadata.gz: 85d86bdf54735a348134ab9abceec3610882fe23
4
+ data.tar.gz: 9f1e65346bd355e4fb1b9f9c83fdf8c45a16a224
5
5
  SHA512:
6
- metadata.gz: dec2d6a557f9c826234c0fdbc0d4bc19eca4e35c28d303d63a9e7078e3e93c72684ea469eb6ae6581a97e2598602b89380d60b19301139ab6b812fcfa0fdc360
7
- data.tar.gz: b71a3dc8ca8bccd242ccfa2148e777c3063b3941a169b50fe09858b25e23df0921f9e42bafc022830d75417cad0e8c51e66996fdc85d6702b50dc74813ac9136
6
+ metadata.gz: e3da11ae5cece8697449bcea8307bc34e1af5b6cb3a39834e96d56f2b32d6306e4ac9070909c79bab0f2486f319a15de408d13df6982d9d3fa367163920c8387
7
+ data.tar.gz: 30b6556d39cf4b06169e518a69fe7a8c1b0b03a05e0f0db97bcd6abafee42625622048885fbc7452dfbf48cec4915983c2feb10aa6aae5ea132a7a7b8c770b56
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ script: bundle exec rake
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in process_tail.gemspec
4
4
  gemspec
5
+
6
+ gem 'pry'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ProcessTail
1
+ # ProcessTail [![Build Status](https://travis-ci.org/hibariya/process_tail.svg?branch=master)](https://travis-ci.org/hibariya/process_tail)
2
2
 
3
3
  https://github.com/hibariya/process_tail
4
4
 
@@ -11,6 +11,7 @@ So you can get other process outputs.
11
11
 
12
12
  * Mac OSX and Windows are not supported at the moment
13
13
  * SEGV occures occasionally
14
+ * Multithreaded process support
14
15
 
15
16
  ## Installation
16
17
 
data/bin/process_tail ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'process_tail'
4
+
5
+ $stdout.sync = true
6
+
7
+ ProcessTail.open Integer(ARGV[0]) do |io|
8
+ while c = io.getc
9
+ print c
10
+ end
11
+ end
@@ -4,10 +4,11 @@ require 'mkmf'
4
4
  abort "missing #{h}" unless have_header(h)
5
5
  end
6
6
 
7
- abort 'missing ptrace' unless have_func('ptrace', 'sys/ptrace.h')
8
-
9
7
  %w(orig_rax rdi rdx rsi rdx).each do |m|
10
8
  abort "missing #{m}" unless have_struct_member('struct user_regs_struct', m, 'sys/user.h')
11
9
  end
12
10
 
11
+ abort 'missing __WALL' unless have_const('__WALL', 'sys/wait.h')
12
+ abort 'missing PTRACE_O_TRACESYSGOOD' unless have_const('PTRACE_O_TRACESYSGOOD', 'sys/ptrace.h')
13
+
13
14
  create_makefile 'process_tail/process_tail'
@@ -1,5 +1,3 @@
1
- #include <errno.h>
2
- #include <stdio.h>
3
1
  #include <stdlib.h>
4
2
  #include <string.h>
5
3
  #include <sys/ptrace.h>
@@ -10,148 +8,257 @@
10
8
  #include <ruby.h>
11
9
  #include <ruby/thread.h>
12
10
 
13
- struct
14
- process_tail_args {
11
+ typedef struct pt_tracee {
15
12
  pid_t pid;
13
+ int activated;
14
+ int dead;
15
+ struct pt_tracee *next;
16
+ } pt_tracee_t;
17
+
18
+ typedef struct pt_wait_args {
16
19
  int *status;
17
- unsigned int fd;
18
- VALUE io;
19
- };
20
+ pid_t pid;
21
+ } pt_wait_args_t;
20
22
 
21
- static void *
22
- process_tail_waitpid(void *argp)
23
+ static pt_tracee_t *
24
+ pt_tracee_add(pt_tracee_t **headpp, pid_t pid)
23
25
  {
24
- struct process_tail_args *args = argp;
26
+ pt_tracee_t *tracee = malloc(sizeof(pt_tracee_t));
25
27
 
26
- waitpid(args->pid, args->status, WUNTRACED | WCONTINUED);
28
+ if (tracee == NULL) {
29
+ exit(EXIT_FAILURE);
30
+ }
27
31
 
28
- return NULL;
32
+ tracee->pid = pid;
33
+ tracee->activated = 0;
34
+ tracee->dead = 0;
35
+ tracee->next = (struct pt_tracee *)*headpp;
36
+
37
+ (*headpp) = tracee;
38
+
39
+ return tracee;
40
+ }
41
+
42
+ static pt_tracee_t *
43
+ pt_tracee_find_or_add(pt_tracee_t **headpp, pid_t pid)
44
+ {
45
+ pt_tracee_t *tracee = *headpp;
46
+
47
+ while (tracee != NULL) {
48
+ if (tracee->pid == pid) {
49
+ return tracee;
50
+ }
51
+
52
+ tracee = tracee->next;
53
+ }
54
+
55
+ return pt_tracee_add(headpp, pid);
29
56
  }
30
57
 
31
58
  static int
32
- process_tail_attach(void *argp)
59
+ pt_tracee_wipedoutq(pt_tracee_t *headp)
33
60
  {
34
- struct process_tail_args *args = argp;
61
+ pt_tracee_t *tracee = headp;
35
62
 
36
- if (ptrace(PTRACE_ATTACH, args->pid, NULL, NULL) < 0) {
37
- rb_sys_fail("PTRACE_ATTACH");
38
- return -1;
63
+ while (tracee != NULL) {
64
+ if (tracee->dead == 0) {
65
+ return 0;
66
+ }
67
+
68
+ tracee = tracee->next;
39
69
  }
40
70
 
41
- rb_thread_call_without_gvl(process_tail_waitpid, (void *)argp, RUBY_UBF_PROCESS, NULL);
71
+ return 1;
72
+ }
73
+
74
+ static void
75
+ pt_tracee_free(pt_tracee_t **headpp)
76
+ {
77
+ pt_tracee_t *tracee = *headpp;
42
78
 
43
- return 0;
79
+ if (tracee->next) {
80
+ pt_tracee_free(&tracee->next);
81
+ }
82
+
83
+ free(tracee);
84
+ (*headpp) = NULL;
44
85
  }
45
86
 
46
- static VALUE
47
- process_tail_detach(VALUE argp)
87
+ static int
88
+ pt_io_closedq(VALUE io)
48
89
  {
49
- struct process_tail_args *args = (void *)argp;
90
+ return rb_funcall(io, rb_intern("closed?"), 0) == Qtrue;
91
+ }
50
92
 
51
- ptrace(PTRACE_DETACH, args->pid, NULL, NULL);
93
+ static void *
94
+ pt_wait(void *argp)
95
+ {
96
+ pt_wait_args_t *args = (pt_wait_args_t *)argp;
52
97
 
53
- return Qnil;
98
+ args->pid = waitpid(-1, args->status, __WALL);
99
+
100
+ return NULL;
54
101
  }
55
102
 
56
103
  static void
57
- process_tail_get_data(pid_t pid, long addr, char *string, int len)
104
+ pt_get_data(pid_t pid, long addr, char *string, int length)
58
105
  {
59
- long data;
60
106
  int i;
107
+ long data;
61
108
 
62
- for (i = 0; i < len; i += sizeof(long)) {
109
+ for (i = 0; i < length; i += sizeof(long)) {
63
110
  data = ptrace(PTRACE_PEEKDATA, pid, addr + i);
64
111
 
65
112
  memcpy(string + i, &data, sizeof(long));
66
113
  }
67
114
 
68
- string[len] = '\0';
115
+ string[length] = '\0';
69
116
  }
70
117
 
71
- static VALUE
72
- process_tail_loop(VALUE argp)
118
+ static void
119
+ pt_ptrace_syscall(pid_t pid, long signal)
73
120
  {
74
- struct process_tail_args *args = (void *)argp;
75
- struct user_regs_struct regs;
76
- int in_syscall = 0;
77
- int signal = 0;
121
+ ptrace(PTRACE_SYSCALL, pid, 0, signal);
122
+
123
+ rb_thread_schedule();
124
+ }
125
+
126
+ static void
127
+ pt_loop(unsigned int fd, VALUE write_io, VALUE read_io, VALUE wait_queue, pt_tracee_t *tracee_headp)
128
+ {
129
+ char *string = NULL;
130
+ int syscall = 0;
78
131
  int status;
79
- char *string = NULL;
132
+ long signal;
133
+ pid_t pid;
134
+ pt_tracee_t *tracee;
135
+ pt_wait_args_t pt_wait_args = {&status};
80
136
 
81
- args->status = &status;
137
+ // NOTE: system call: eax, arguments: rdi, rsi, rdx, r10, r8, r9
138
+ struct user_regs_struct regs;
82
139
 
83
- while (ptrace(PTRACE_SYSCALL, args->pid, NULL, signal) == 0) {
84
- rb_thread_call_without_gvl(process_tail_waitpid, args, RUBY_UBF_PROCESS, NULL);
140
+ while (1) {
141
+ rb_thread_call_without_gvl(pt_wait, &pt_wait_args, RUBY_UBF_PROCESS, NULL);
85
142
 
86
143
  signal = 0;
144
+ pid = pt_wait_args.pid;
145
+ tracee = pt_tracee_find_or_add(&tracee_headp, pid);
146
+
147
+ if (pt_io_closedq(read_io)) {
148
+ pt_tracee_free(&tracee_headp);
87
149
 
88
- if (WIFEXITED(status)) {
89
- return Qnil;
150
+ return;
90
151
  }
91
152
 
92
- if (!WIFSTOPPED(status)) {
153
+ if (tracee->activated == 0) {
154
+ ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD);
155
+ tracee->activated = 1;
156
+
157
+ rb_funcall(wait_queue, rb_intern("enq"), 1, INT2FIX((int)pid));
158
+ pt_ptrace_syscall(pid, signal);
159
+
93
160
  continue;
94
161
  }
95
162
 
96
- switch (WSTOPSIG(status)) {
97
- case SIGTRAP:
98
- ptrace(PTRACE_GETREGS, args->pid, 0, &regs);
99
-
100
- if (regs.orig_rax != SYS_write) {
101
- continue;
102
- }
103
-
104
- in_syscall = 1 - in_syscall;
105
- if (in_syscall && (args->fd == 0 || regs.rdi == args->fd)) {
106
- string = malloc(regs.rdx + sizeof(long));
107
- process_tail_get_data(args->pid, regs.rsi, string, regs.rdx);
108
-
109
- if (rb_funcall(args->io, rb_intern("closed?"), 0) == Qtrue) {
110
- return Qnil;
111
- }
112
-
113
- rb_io_write(args->io, rb_str_new_cstr(string));
114
-
115
- free(string);
116
- }
117
- case SIGVTALRM:
118
- break;
119
- case SIGSTOP:
120
- break;
121
- default:
122
- signal = WSTOPSIG(status);
163
+ if (WIFEXITED(status) || WIFSIGNALED(status)) {
164
+ tracee->dead = 1;
165
+
166
+ if (pt_tracee_wipedoutq(tracee_headp)) {
167
+ pt_tracee_free(&tracee_headp);
168
+
169
+ return;
170
+ }
171
+
172
+ continue;
123
173
  }
124
174
 
125
- rb_thread_schedule();
126
- }
175
+ if (!(WIFSTOPPED(status) && WSTOPSIG(status) & 0x80)) {
176
+ signal = WSTOPSIG(status);
177
+ pt_ptrace_syscall(pid, signal);
127
178
 
128
- return Qnil;
179
+ continue;
180
+ }
181
+
182
+ ptrace(PTRACE_GETREGS, pid, 0, &regs);
183
+
184
+ if (regs.orig_rax != SYS_write) {
185
+ pt_ptrace_syscall(pid, signal);
186
+
187
+ continue;
188
+ }
189
+
190
+ syscall = 1 - syscall;
191
+ if (syscall && (fd == 0 || regs.rdi == fd)) {
192
+ string = malloc(regs.rdx + sizeof(long));
193
+
194
+ if (string == NULL) {
195
+ exit(EXIT_FAILURE);
196
+ }
197
+
198
+ pt_get_data(pid, regs.rsi, string, regs.rdx);
199
+
200
+ if (pt_io_closedq(read_io)) {
201
+ free(string);
202
+ pt_tracee_free(&tracee_headp);
203
+
204
+ return;
205
+ }
206
+
207
+ rb_io_write(write_io, rb_str_new_cstr(string));
208
+
209
+ free(string);
210
+ }
211
+
212
+ pt_ptrace_syscall(pid, signal);
213
+ }
129
214
  }
130
215
 
131
216
  static VALUE
132
- process_tail_trace(VALUE klass, VALUE pidp, VALUE fdp, VALUE io)
217
+ pt_attach(VALUE klass, VALUE pidv)
133
218
  {
134
- pid_t pid = (pid_t)FIX2INT(pidp);
135
- unsigned int fd = (unsigned int)FIX2INT(fdp);
219
+ int pid = (pid_t)FIX2INT(pidv);
136
220
 
137
- struct process_tail_args args = {pid, NULL, fd, io};
221
+ Check_Type(pidv, T_FIXNUM);
138
222
 
139
- Check_Type(pidp, T_FIXNUM);
140
- Check_Type(fdp, T_FIXNUM);
141
- Check_Type(io, T_FILE);
223
+ if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
224
+ rb_sys_fail("PTRACE_ATTACH");
142
225
 
143
- if (process_tail_attach((void *)&args) == 0) {
144
- rb_ensure(process_tail_loop, (VALUE)&args, process_tail_detach, (VALUE)&args);
226
+ return Qnil;
145
227
  }
146
228
 
229
+ return Qtrue;
230
+ }
231
+
232
+ static VALUE
233
+ pt_detach(VALUE klass, VALUE pidv)
234
+ {
235
+ Check_Type(pidv, T_FIXNUM);
236
+
237
+ ptrace(PTRACE_DETACH, (pid_t)FIX2INT(pidv), NULL, NULL);
238
+
239
+ return Qnil;
240
+ }
241
+
242
+ static VALUE
243
+ pt_trace(VALUE klass, VALUE fdp, VALUE write_io, VALUE read_io, VALUE wait_queue)
244
+ {
245
+ pt_tracee_t *tracee_headp = NULL;
246
+
247
+ Check_Type(fdp, T_FIXNUM);
248
+ Check_Type(write_io, T_FILE);
249
+ Check_Type(read_io, T_FILE);
250
+
251
+ pt_loop((unsigned int)FIX2INT(fdp), write_io, read_io, wait_queue, tracee_headp);
252
+
147
253
  return Qnil;
148
254
  }
149
255
 
150
256
  void
151
257
  Init_process_tail()
152
258
  {
153
- VALUE ProcessTail;
259
+ VALUE ProcessTail = rb_define_module("ProcessTail");
154
260
 
155
- ProcessTail = rb_define_module("ProcessTail");
156
- rb_define_singleton_method(ProcessTail, "do_trace", process_tail_trace, 3);
261
+ rb_define_singleton_method(ProcessTail, "attach", pt_attach, 1);
262
+ rb_define_singleton_method(ProcessTail, "do_trace", pt_trace, 4);
263
+ rb_define_singleton_method(ProcessTail, "detach", pt_detach, 1);
157
264
  }
@@ -1,3 +1,3 @@
1
1
  module ProcessTail
2
- VERSION = '0.0.1'
2
+ VERSION = '0.0.2'
3
3
  end
data/lib/process_tail.rb CHANGED
@@ -1,37 +1,68 @@
1
+ require 'thread'
1
2
  require 'process_tail/process_tail'
2
3
  require 'process_tail/version'
3
4
 
4
5
  module ProcessTail
6
+ TRACE_LOCK = Mutex.new # NOTE For now, ProcessTail can't trace multiple processes at the same time
7
+
5
8
  class << self
6
9
  def open(pid, fd = :stdout)
7
- read_io, thread = trace(pid, fd)
10
+ io = trace(pid, fd)
8
11
 
9
- block_given? ? yield(read_io) : read_io
12
+ block_given? ? yield(io) : io
10
13
  ensure
11
- finalize = -> {
12
- read_io.close unless read_io.closed?
13
- thread.kill
14
- }
15
-
16
- block_given? ? finalize.call : at_exit(&finalize)
14
+ io.close if block_given? && !io.closed?
17
15
  end
18
16
 
19
17
  def trace(pid, fd = :stdout)
18
+ TRACE_LOCK.synchronize {
19
+ trace_without_lock(pid, fd)
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def trace_without_lock(pid, fd)
20
26
  read_io, write_io = IO.pipe
27
+ task_ids = extract_task_ids(pid)
28
+ wait_queue = Queue.new
21
29
 
22
30
  thread = Thread.fork {
23
31
  begin
24
- do_trace pid, extract_fd(fd), write_io
32
+ extract_task_ids(pid).each do |tid|
33
+ attach tid
34
+ end
35
+
36
+ do_trace extract_fd(fd), write_io, read_io, wait_queue
25
37
  ensure
26
- begin
27
- [read_io, write_io].each do |io|
28
- io.close unless io.closed?
29
- end
30
- rescue IOError; end
38
+ write_io.close unless write_io.closed?
39
+
40
+ task_ids.each do |tid|
41
+ detach tid
42
+ end
31
43
  end
32
44
  }
33
45
 
34
- [read_io, thread]
46
+ at_exit do
47
+ thread.kill
48
+ thread.join
49
+ end
50
+
51
+ wait_all_attach task_ids, wait_queue
52
+
53
+ read_io
54
+ end
55
+
56
+ def wait_all_attach(task_ids, waitq)
57
+ task_ids.size.times do
58
+ waitq.deq
59
+ end
60
+ end
61
+
62
+ def extract_task_ids(pid)
63
+ Dir.open("/proc/#{pid}/task") {|dir|
64
+ dir.entries.grep(/^\d+$/).map(&:to_i)
65
+ }
35
66
  end
36
67
 
37
68
  def extract_fd(name)
@@ -1,24 +1,27 @@
1
1
  require 'process_tail'
2
+ require 'timeout'
2
3
 
3
4
  describe ProcessTail do
4
5
  def process_maker(&fn)
5
6
  -> {
6
- Process.fork {
7
+ pid = Process.fork {
7
8
  trap :USR1 do
8
- sleep 0.1 # XXX :(
9
-
10
9
  fn.call if fn
11
10
  end
12
11
 
13
12
  sleep
14
13
  }
14
+
15
+ $recorded_children << pid
16
+
17
+ pid
15
18
  }
16
19
  end
17
20
 
18
21
  def expect_same_output_and_no_error(fd, method, string)
19
- greeting = process_maker { send(method, string) }
20
- pid = greeting.call
21
- io, th = ProcessTail.trace(pid, fd)
22
+ greeting = process_maker { send(method, string) }
23
+ pid = greeting.call
24
+ io = ProcessTail.trace(pid, fd)
22
25
 
23
26
  Process.kill :USR1, pid # resume child
24
27
 
@@ -26,9 +29,23 @@ describe ProcessTail do
26
29
  expect(io.gets).to eq expected
27
30
  end
28
31
 
29
- Process.kill :TERM, pid
32
+ io.close
33
+ end
34
+
35
+ around do |example|
36
+ begin
37
+ $recorded_children = []
38
+
39
+ # at least every examples should be finished within 5 seconds
40
+ timeout 5 do
41
+ example.run
42
+ end
30
43
 
31
- expect { th.join }.to_not raise_error
44
+ ensure
45
+ $recorded_children.each do |child|
46
+ Process.kill :TERM, child
47
+ end
48
+ end
32
49
  end
33
50
 
34
51
  describe '.open' do
@@ -46,7 +63,7 @@ describe ProcessTail do
46
63
  pid = process_maker.call
47
64
  read_io = nil
48
65
 
49
- ProcessTail.open pid, :stdout do |io|
66
+ ProcessTail.open pid, :stdout do |io|
50
67
  read_io = io
51
68
 
52
69
  expect(read_io).to_not be_closed
@@ -57,9 +74,44 @@ describe ProcessTail do
57
74
  end
58
75
 
59
76
  describe '.trace' do
60
- it do
77
+ specify 'simple stdout' do
61
78
  expect_same_output_and_no_error :stdout, :puts, 'HELLO'
79
+ end
80
+
81
+ specify 'simple stderr' do
62
82
  expect_same_output_and_no_error :stderr, :warn, 'HELLO'
83
+ end
84
+
85
+ specify 'multithreaded' do
86
+ pid = Process.fork {
87
+ trap :USR1 do
88
+ puts 'HELLO'
89
+ end
90
+
91
+ Thread.fork do
92
+ trap :USR2 do
93
+ puts 'HELLO'
94
+ end
95
+
96
+ sleep
97
+ end
98
+
99
+ sleep
100
+ }
101
+
102
+ $recorded_children << pid
103
+ io = ProcessTail.trace(pid, :stdout)
104
+
105
+ Process.kill :USR1, pid
106
+ Process.kill :USR2, pid
107
+
108
+ expect(io.gets).to eq "HELLO\n"
109
+ expect(io.gets).to eq "HELLO\n"
110
+
111
+ io.close
112
+ end
113
+
114
+ specify 'multiline' do
63
115
  expect_same_output_and_no_error :stdout, :puts, "HELLO\nLOVE AND KISSES"
64
116
  end
65
117
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_tail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hika Hibariya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-03 00:00:00.000000000 Z
11
+ date: 2015-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -69,16 +69,19 @@ dependencies:
69
69
  description:
70
70
  email:
71
71
  - hibariya@gmail.com
72
- executables: []
72
+ executables:
73
+ - process_tail
73
74
  extensions:
74
75
  - ext/process_tail/extconf.rb
75
76
  extra_rdoc_files: []
76
77
  files:
77
78
  - ".gitignore"
79
+ - ".travis.yml"
78
80
  - Gemfile
79
81
  - LICENSE.txt
80
82
  - README.md
81
83
  - Rakefile
84
+ - bin/process_tail
82
85
  - ext/process_tail/extconf.rb
83
86
  - ext/process_tail/process_tail.c
84
87
  - lib/process_tail.rb