process_tail 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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