scmd 2.1.1 → 2.1.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.
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
+