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 +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
|
+
|