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 +2 -0
- data/LICENSE.txt +1 -1
- data/Rakefile +1 -1
- data/bench/results.txt +80 -0
- data/bench/runner.rb +29 -0
- data/lib/scmd.rb +9 -0
- data/lib/scmd/command.rb +120 -56
- data/lib/scmd/version.rb +1 -1
- data/log/.gitkeep +0 -0
- data/scmd.gemspec +2 -1
- data/script/bench.rb +47 -0
- data/test/helper.rb +6 -2
- data/test/support/bigger-than-64k.txt +704 -0
- data/test/support/smaller-than-64k.txt +1 -0
- data/test/unit/command_tests.rb +43 -20
- data/test/unit/scmd_tests.rb +49 -6
- metadata +23 -13
data/Gemfile
CHANGED
data/LICENSE.txt
CHANGED
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
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 [31m+ 0 MB, 0%[0m
|
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 [32m- 0 MB, 0%[0m
|
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 [32m- 0 MB, 0%[0m
|
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 [31m+ 3 MB, 7%[0m
|
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 [32m- 0 MB, 0%[0m
|
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 [31m+ 2 MB, 4%[0m
|
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 [31m+ 42 MB, 98%[0m
|
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 [31m+ 421 MB, 497%[0m
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
-
|
52
|
-
|
53
|
-
@pid = @
|
54
|
-
|
55
|
-
|
56
|
-
@
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
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 ||
|
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
|
-
!@
|
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
|
-
|
112
|
-
|
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
|
127
|
-
@
|
128
|
-
@stdout = @stderr = ''
|
125
|
+
def reset_attrs
|
126
|
+
@stdout, @stderr, @pid, @exitstatus = '', '', nil, nil
|
129
127
|
end
|
130
128
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@
|
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, @
|
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
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
|
+
|