scmd 2.1.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,3 +3,5 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rake'
6
+ gem 'pry'
7
+ gem "whysoslow"
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-Present Kelly Redding, Collin Redding
1
+ Copyright (c) 2012-Present Kelly Redding and Collin Redding
2
2
 
3
3
  MIT License
4
4
 
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
data/bench/results.txt ADDED
@@ -0,0 +1,80 @@
1
+ echo hi: 1 times
2
+ ----------------
3
+ whysoslow? ..
4
+
5
+ mem @ start 29 MB ??
6
+ mem @ finish 29 MB + 0 MB, 0%
7
+
8
+ user system total real
9
+ time 0.0 ms 0.0 ms 0.0 ms 1.928 ms
10
+
11
+ echo hi: 10 times
12
+ -----------------
13
+ whysoslow? ..
14
+
15
+ mem @ start 38 MB ??
16
+ mem @ finish 38 MB - 0 MB, 0%
17
+
18
+ user system total real
19
+ time 0.0 ms 0.0 ms 10.0 ms 16.874 ms
20
+
21
+ echo hi: 100 times
22
+ ------------------
23
+ whysoslow? ..
24
+
25
+ mem @ start 38 MB ??
26
+ mem @ finish 38 MB - 0 MB, 0%
27
+
28
+ user system total real
29
+ time 20.0 ms 20.0 ms 130.0 ms 147.925 ms
30
+
31
+ echo hi: 1000 times
32
+ -------------------
33
+ whysoslow? ..
34
+
35
+ mem @ start 38 MB ??
36
+ mem @ finish 41 MB + 3 MB, 7%
37
+
38
+ user system total real
39
+ time 170.0 ms 180.0 ms 1320.0 ms 1390.406 ms
40
+
41
+ cat test/support/bigger-than-64k.txt: 1 times
42
+ ---------------------------------------------
43
+ whysoslow? ..
44
+
45
+ mem @ start 41 MB ??
46
+ mem @ finish 41 MB - 0 MB, 0%
47
+
48
+ user system total real
49
+ time 0.0 ms 0.0 ms 0.0 ms 3.487 ms
50
+
51
+ cat test/support/bigger-than-64k.txt: 10 times
52
+ ----------------------------------------------
53
+ whysoslow? ..
54
+
55
+ mem @ start 41 MB ??
56
+ mem @ finish 43 MB + 2 MB, 4%
57
+
58
+ user system total real
59
+ time 0.0 ms 0.0 ms 20.0 ms 30.533 ms
60
+
61
+ cat test/support/bigger-than-64k.txt: 100 times
62
+ -----------------------------------------------
63
+ whysoslow? ..
64
+
65
+ mem @ start 43 MB ??
66
+ mem @ finish 85 MB + 42 MB, 98%
67
+
68
+ user system total real
69
+ time 40.0 ms 40.0 ms 250.0 ms 274.06 ms
70
+
71
+ cat test/support/bigger-than-64k.txt: 1000 times
72
+ ------------------------------------------------
73
+ whysoslow? ..
74
+
75
+ mem @ start 85 MB ??
76
+ mem @ finish 506 MB + 421 MB, 497%
77
+
78
+ user system total real
79
+ time 360.0 ms 400.0 ms 2540.0 ms 2649.686 ms
80
+
data/bench/runner.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'whysoslow'
2
+ require 'scmd'
3
+
4
+ class ScmdBenchRunner
5
+
6
+ attr_reader :result
7
+
8
+ def self.run(*args)
9
+ self.new(*args).run
10
+ end
11
+
12
+ def initialize(printer_io, cmd, num_times = 10)
13
+ @cmd = cmd
14
+ @proc = proc do
15
+ num_times.times{ cmd.run! }
16
+ end
17
+
18
+ @printer = Whysoslow::DefaultPrinter.new(printer_io, {
19
+ :title => "#{@cmd.cmd_str}: #{num_times} times",
20
+ :verbose => true
21
+ })
22
+ @runner = Whysoslow::Runner.new(@printer)
23
+ end
24
+
25
+ def run
26
+ @runner.run &@proc
27
+ end
28
+
29
+ end
data/lib/scmd.rb CHANGED
@@ -7,4 +7,13 @@ module Scmd
7
7
  Command.new(*args, &block)
8
8
  end
9
9
 
10
+ TimeoutError = Class.new(::RuntimeError)
11
+
12
+ class RunError < ::RuntimeError
13
+ def initialize(stderr, called_from = nil)
14
+ super(stderr)
15
+ set_backtrace(called_from || caller)
16
+ end
17
+ end
18
+
10
19
  end
data/lib/scmd/command.rb CHANGED
@@ -1,4 +1,6 @@
1
+ require 'thread'
1
2
  require 'posix-spawn'
3
+ require 'scmd'
2
4
 
3
5
  # Scmd::Command is a base wrapper for handling system commands. Initialize it
4
6
  # with with a string specifying the command to execute. You can then run the
@@ -7,73 +9,77 @@ require 'posix-spawn'
7
9
 
8
10
  module Scmd
9
11
 
10
- class RunError < ::RuntimeError
11
- def initialize(stderr, called_from)
12
- super(stderr)
13
- set_backtrace(called_from)
14
- end
15
- end
16
-
17
- TimeoutError = Class.new(::RuntimeError)
18
-
19
12
  class Command
20
- WAIT_INTERVAL = 0.1 # seconds
21
- STOP_TIMEOUT = 3 # seconds
22
- RunData = Class.new(Struct.new(:pid, :stdin, :stdout, :stderr))
13
+ READ_SIZE = 10240 # bytes
14
+ READ_CHECK_TIMEOUT = 0.001 # seconds
15
+ DEFAULT_STOP_TIMEOUT = 3 # seconds
23
16
 
24
17
  attr_reader :cmd_str
25
18
  attr_reader :pid, :exitstatus, :stdout, :stderr
26
19
 
27
20
  def initialize(cmd_str)
28
21
  @cmd_str = cmd_str
29
- setup
22
+ reset_attrs
30
23
  end
31
24
 
32
- def run(input=nil)
25
+ def run(input = nil)
33
26
  run!(input) rescue RunError
34
27
  self
35
28
  end
36
29
 
37
- def run!(input=nil)
38
- called_from = caller
39
-
30
+ def run!(input = nil)
31
+ start_err_msg, start_err_bt = nil, nil
40
32
  begin
41
33
  start(input)
34
+ rescue StandardError => err
35
+ start_err_msg, start_err_bt = err.message, err.backtrace
42
36
  ensure
43
37
  wait # indefinitely until cmd is done running
44
- raise RunError.new(@stderr, called_from) if !success?
38
+ raise RunError.new(start_err_msg || @stderr, start_err_bt || caller) if !success?
45
39
  end
46
40
 
47
41
  self
48
42
  end
49
43
 
50
- def start(input=nil)
51
- setup
52
- @run_data = RunData.new(*POSIX::Spawn::popen4(@cmd_str))
53
- @pid = @run_data.pid.to_i
54
- if !input.nil?
55
- [*input].each{|line| @run_data.stdin.puts line.to_s}
56
- @run_data.stdin.close
44
+ def start(input = nil)
45
+ setup_run
46
+
47
+ @pid = @child_process.pid.to_i
48
+ @child_process.write(input)
49
+ @read_output_thread = Thread.new do
50
+ while @child_process.check_for_exit
51
+ begin
52
+ read_output
53
+ rescue EOFError => err
54
+ end
55
+ end
56
+ @stop_w.write_nonblock('.')
57
57
  end
58
58
  end
59
59
 
60
- def wait(timeout=nil)
60
+ def wait(timeout = nil)
61
61
  return if !running?
62
62
 
63
- pidnum, pidstatus = wait_for_exit(timeout)
64
- @stdout += @run_data.stdout.read.strip
65
- @stderr += @run_data.stderr.read.strip
66
- @exitstatus = pidstatus.exitstatus || pidstatus.termsig
63
+ wait_for_exit(timeout)
64
+ if @child_process.running?
65
+ kill
66
+ raise(TimeoutError, "`#{@cmd_str}` timed out (#{timeout}s).")
67
+ end
68
+ @read_output_thread.join
67
69
 
68
- teardown
70
+ @stdout << @child_process.flush_stdout
71
+ @stderr << @child_process.flush_stderr
72
+ @exitstatus = @child_process.exitstatus
73
+
74
+ teardown_run
69
75
  end
70
76
 
71
- def stop(timeout=nil)
77
+ def stop(timeout = nil)
72
78
  return if !running?
73
79
 
74
80
  send_term
75
81
  begin
76
- wait(timeout || STOP_TIMEOUT)
82
+ wait(timeout || DEFAULT_STOP_TIMEOUT)
77
83
  rescue TimeoutError => err
78
84
  kill
79
85
  end
@@ -87,7 +93,7 @@ module Scmd
87
93
  end
88
94
 
89
95
  def running?
90
- !@run_data.nil?
96
+ !@child_process.nil?
91
97
  end
92
98
 
93
99
  def success?
@@ -107,32 +113,31 @@ module Scmd
107
113
 
108
114
  private
109
115
 
116
+ def read_output
117
+ @child_process.read(READ_SIZE){ |out, err| @stdout += out; @stderr += err }
118
+ end
119
+
110
120
  def wait_for_exit(timeout)
111
- if timeout.nil?
112
- ::Process::waitpid2(@run_data.pid)
113
- else
114
- timeout_time = Time.now + timeout
115
- pid, status = nil, nil
116
- while pid.nil? && Time.now < timeout_time
117
- sleep WAIT_INTERVAL
118
- pid, status = ::Process.waitpid2(@run_data.pid, ::Process::WNOHANG)
119
- pid = nil if pid == 0 # may happen on jruby
120
- end
121
- raise(TimeoutError, "`#{@cmd_str}` timed out (#{timeout}s).") if pid.nil?
122
- [pid, status]
123
- end
121
+ ios, _, _ = IO.select([ @stop_r ], nil, nil, timeout)
122
+ @stop_r.read_nonblock(1) if ios && ios.include?(@stop_r)
124
123
  end
125
124
 
126
- def setup
127
- @pid = @exitstatus = @run_data = nil
128
- @stdout = @stderr = ''
125
+ def reset_attrs
126
+ @stdout, @stderr, @pid, @exitstatus = '', '', nil, nil
129
127
  end
130
128
 
131
- def teardown
132
- [@run_data.stdin, @run_data.stdout, @run_data.stderr].each do |io|
133
- io.close if !io.closed?
134
- end
135
- @run_data = nil
129
+ def setup_run
130
+ reset_attrs
131
+ @stop_r, @stop_w = IO.pipe
132
+ @read_output_thread = nil
133
+ @child_process = ChildProcess.new(@cmd_str)
134
+ end
135
+
136
+ def teardown_run
137
+ @child_process.teardown
138
+ [@stop_r, @stop_w].each{ |fd| fd.close if fd && !fd.closed? }
139
+ @stop_r, @stop_w = nil, nil
140
+ @child_process, @read_output_thread = nil, nil
136
141
  true
137
142
  end
138
143
 
@@ -146,7 +151,66 @@ module Scmd
146
151
 
147
152
  def send_signal(sig)
148
153
  return if !running?
149
- ::Process.kill sig, @run_data.pid
154
+ ::Process.kill sig, @child_process.pid
155
+ end
156
+
157
+ class ChildProcess
158
+
159
+ attr_reader :pid, :stdin, :stdout, :stderr
160
+
161
+ def initialize(cmd_str)
162
+ @pid, @stdin, @stdout, @stderr = *::POSIX::Spawn::popen4(cmd_str)
163
+ @wait_pid, @wait_status = nil, nil
164
+ end
165
+
166
+ def check_for_exit
167
+ if @wait_pid.nil?
168
+ @wait_pid, @wait_status = ::Process.waitpid2(@pid, ::Process::WNOHANG)
169
+ @wait_pid = nil if @wait_pid == 0 # may happen on jruby
170
+ end
171
+ @wait_pid.nil?
172
+ end
173
+
174
+ def running?
175
+ @wait_pid.nil?
176
+ end
177
+
178
+ def exitstatus
179
+ return nil if @wait_status.nil?
180
+ @wait_status.exitstatus || @wait_status.termsig
181
+ end
182
+
183
+ def write(input)
184
+ if !input.nil?
185
+ [*input].each{ |line| @stdin.puts line.to_s }
186
+ @stdin.close
187
+ end
188
+ end
189
+
190
+ def read(size)
191
+ ios, _, _ = IO.select([ @stdout, @stderr ], nil, nil, READ_CHECK_TIMEOUT)
192
+ if ios && block_given?
193
+ yield read_if_ready(ios, @stdout, size), read_if_ready(ios, @stderr, size)
194
+ end
195
+ end
196
+
197
+ def flush_stdout; @stdout.read; end
198
+ def flush_stderr; @stderr.read; end
199
+
200
+ def teardown
201
+ [@stdin, @stdout, @stderr].each{ |fd| fd.close if fd && !fd.closed? }
202
+ end
203
+
204
+ private
205
+
206
+ def read_if_ready(ready_ios, io, size)
207
+ ready_ios.include?(io) ? read_by_size(io, size) : ''
208
+ end
209
+
210
+ def read_by_size(io, size)
211
+ io.read_nonblock(size)
212
+ end
213
+
150
214
  end
151
215
 
152
216
  end
data/lib/scmd/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Scmd
2
- VERSION = "2.1.1"
2
+ VERSION = "2.1.2"
3
3
  end
data/log/.gitkeep ADDED
File without changes
data/scmd.gemspec CHANGED
@@ -11,12 +11,13 @@ Gem::Specification.new do |gem|
11
11
  gem.description = %q{Build and run system commands.}
12
12
  gem.summary = %q{Build and run system commands.}
13
13
  gem.homepage = "http://github.com/redding/scmd"
14
+ gem.license = 'MIT'
14
15
 
15
16
  gem.files = `git ls-files`.split($/)
16
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
19
  gem.require_paths = ["lib"]
19
20
 
20
- gem.add_development_dependency("assert")
21
+ gem.add_development_dependency("assert", ["~> 2.3"])
21
22
  gem.add_dependency("posix-spawn")
22
23
  end
data/script/bench.rb ADDED
@@ -0,0 +1,47 @@
1
+ # $ bundle exec ruby script/bench.rb
2
+
3
+ require 'bench/runner'
4
+
5
+ class ScmdBenchLogger
6
+
7
+ def initialize(file_path)
8
+ @file = File.open(file_path, 'w')
9
+ @ios = [@file, $stdout]
10
+ yield self
11
+ @file.close
12
+ end
13
+
14
+ def method_missing(meth, *args, &block)
15
+ @ios.each do |io|
16
+ io.respond_to?(meth.to_s) ? io.send(meth.to_s, *args, &block) : super
17
+ end
18
+ end
19
+
20
+ def respond_to?(*args)
21
+ @ios.first.respond_to?(args.first.to_s) ? true : super
22
+ end
23
+
24
+ end
25
+
26
+ def run_cmd(logger, *args)
27
+ GC.disable
28
+
29
+ ScmdBenchRunner.run(logger, *args)
30
+ logger.puts
31
+
32
+ GC.enable
33
+ GC.start
34
+ end
35
+
36
+ ScmdBenchLogger.new('bench/results.txt') do |logger|
37
+ run_cmd(logger, Scmd.new("echo hi"), 1)
38
+ run_cmd(logger, Scmd.new("echo hi"), 10)
39
+ run_cmd(logger, Scmd.new("echo hi"), 100)
40
+ run_cmd(logger, Scmd.new("echo hi"), 1000)
41
+
42
+ run_cmd(logger, Scmd.new("cat test/support/bigger-than-64k.txt"), 1)
43
+ run_cmd(logger, Scmd.new("cat test/support/bigger-than-64k.txt"), 10)
44
+ run_cmd(logger, Scmd.new("cat test/support/bigger-than-64k.txt"), 100)
45
+ run_cmd(logger, Scmd.new("cat test/support/bigger-than-64k.txt"), 1000)
46
+ end
47
+