scmd 2.0.0 → 2.1.1
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/.gitignore +1 -2
- data/Gemfile +2 -4
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.md +39 -15
- data/Rakefile +0 -7
- data/lib/scmd.rb +1 -0
- data/lib/scmd/command.rb +104 -33
- data/lib/scmd/version.rb +1 -1
- data/scmd.gemspec +8 -7
- data/test/helper.rb +2 -2
- data/test/{command_tests.rb → unit/command_tests.rb} +80 -1
- data/test/{scmd_tests.rb → unit/scmd_tests.rb} +0 -0
- metadata +13 -16
- data/test/irb.rb +0 -9
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/{LICENSE → LICENSE.txt}
RENAMED
data/README.md
CHANGED
@@ -1,20 +1,6 @@
|
|
1
1
|
# Scmd
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
## Installation
|
6
|
-
|
7
|
-
Add this line to your application's Gemfile:
|
8
|
-
|
9
|
-
gem 'scmd'
|
10
|
-
|
11
|
-
And then execute:
|
12
|
-
|
13
|
-
$ bundle
|
14
|
-
|
15
|
-
Or install it yourself as:
|
16
|
-
|
17
|
-
$ gem install scmd
|
3
|
+
Build and run system commands. Scmd uses `posix-spawn` to fork child processes to run the commands.
|
18
4
|
|
19
5
|
## Usage
|
20
6
|
|
@@ -38,6 +24,30 @@ Run it:
|
|
38
24
|
cmd.run
|
39
25
|
```
|
40
26
|
|
27
|
+
**OR**, async run it:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
cmd.start
|
31
|
+
cmd.running? # => true
|
32
|
+
cmd.pid #=> 12345
|
33
|
+
|
34
|
+
# do other stuff...
|
35
|
+
cmd.wait # indefinitely until cmd exits
|
36
|
+
```
|
37
|
+
|
38
|
+
**OR**, async run it with a timeout:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
cmd.start
|
42
|
+
|
43
|
+
begin
|
44
|
+
cmd.wait(10)
|
45
|
+
rescue Scmd::Timeout => err
|
46
|
+
cmd.stop # attempt to stop the cmd nicely, kill if doesn't stop in time
|
47
|
+
cmd.kill # just kill the cmd now
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
41
51
|
Results:
|
42
52
|
|
43
53
|
```ruby
|
@@ -84,6 +94,20 @@ Raise an exception if not successful with `run!`:
|
|
84
94
|
Scmd.new("cd /path/that/does/not/exist").run! #=> Scmd::Command::Failure
|
85
95
|
```
|
86
96
|
|
97
|
+
## Installation
|
98
|
+
|
99
|
+
Add this line to your application's Gemfile:
|
100
|
+
|
101
|
+
gem 'scmd'
|
102
|
+
|
103
|
+
And then execute:
|
104
|
+
|
105
|
+
$ bundle
|
106
|
+
|
107
|
+
Or install it yourself as:
|
108
|
+
|
109
|
+
$ gem install scmd
|
110
|
+
|
87
111
|
## Contributing
|
88
112
|
|
89
113
|
1. Fork it
|
data/Rakefile
CHANGED
data/lib/scmd.rb
CHANGED
data/lib/scmd/command.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
+
require 'posix-spawn'
|
2
|
+
|
1
3
|
# Scmd::Command is a base wrapper for handling system commands. Initialize it
|
2
4
|
# with with a string specifying the command to execute. You can then run the
|
3
5
|
# command and inspect its results. It can be used as is, or inherited from to
|
4
6
|
# create a more custom command wrapper.
|
5
7
|
|
6
|
-
require 'posix-spawn'
|
7
|
-
|
8
8
|
module Scmd
|
9
9
|
|
10
10
|
class RunError < ::RuntimeError
|
@@ -14,19 +14,80 @@ module Scmd
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
TimeoutError = Class.new(::RuntimeError)
|
18
|
+
|
17
19
|
class Command
|
20
|
+
WAIT_INTERVAL = 0.1 # seconds
|
21
|
+
STOP_TIMEOUT = 3 # seconds
|
22
|
+
RunData = Class.new(Struct.new(:pid, :stdin, :stdout, :stderr))
|
18
23
|
|
19
24
|
attr_reader :cmd_str
|
20
25
|
attr_reader :pid, :exitstatus, :stdout, :stderr
|
21
26
|
|
22
27
|
def initialize(cmd_str)
|
23
28
|
@cmd_str = cmd_str
|
24
|
-
|
29
|
+
setup
|
25
30
|
end
|
26
31
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
32
|
+
def run(input=nil)
|
33
|
+
run!(input) rescue RunError
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def run!(input=nil)
|
38
|
+
called_from = caller
|
39
|
+
|
40
|
+
begin
|
41
|
+
start(input)
|
42
|
+
ensure
|
43
|
+
wait # indefinitely until cmd is done running
|
44
|
+
raise RunError.new(@stderr, called_from) if !success?
|
45
|
+
end
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
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
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def wait(timeout=nil)
|
61
|
+
return if !running?
|
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
|
67
|
+
|
68
|
+
teardown
|
69
|
+
end
|
70
|
+
|
71
|
+
def stop(timeout=nil)
|
72
|
+
return if !running?
|
73
|
+
|
74
|
+
send_term
|
75
|
+
begin
|
76
|
+
wait(timeout || STOP_TIMEOUT)
|
77
|
+
rescue TimeoutError => err
|
78
|
+
kill
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def kill
|
83
|
+
return if !running?
|
84
|
+
|
85
|
+
send_kill
|
86
|
+
wait # indefinitely until cmd is killed
|
87
|
+
end
|
88
|
+
|
89
|
+
def running?
|
90
|
+
!@run_data.nil?
|
30
91
|
end
|
31
92
|
|
32
93
|
def success?
|
@@ -44,40 +105,50 @@ module Scmd
|
|
44
105
|
" @exitstatus=#{@exitstatus.inspect}>"
|
45
106
|
end
|
46
107
|
|
47
|
-
|
48
|
-
|
49
|
-
|
108
|
+
private
|
109
|
+
|
110
|
+
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
|
50
124
|
end
|
51
125
|
|
52
|
-
def
|
53
|
-
|
126
|
+
def setup
|
127
|
+
@pid = @exitstatus = @run_data = nil
|
128
|
+
@stdout = @stderr = ''
|
129
|
+
end
|
54
130
|
|
55
|
-
|
56
|
-
|
57
|
-
if !
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
@stdout += stdout.read.strip
|
63
|
-
@stderr += stderr.read.strip
|
64
|
-
rescue Errno::ENOENT => err
|
65
|
-
@exitstatus = -1
|
66
|
-
@stderr = err.message
|
67
|
-
ensure
|
68
|
-
[stdin, stdout, stderr].each{|io| io.close if !io.closed?}
|
69
|
-
::Process::waitpid(pid)
|
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
|
136
|
+
true
|
137
|
+
end
|
70
138
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
@exitstatus ||= $?.exitstatus
|
139
|
+
def send_term
|
140
|
+
send_signal 'TERM'
|
141
|
+
end
|
75
142
|
|
76
|
-
|
77
|
-
|
143
|
+
def send_kill
|
144
|
+
send_signal 'KILL'
|
145
|
+
end
|
78
146
|
|
79
|
-
|
147
|
+
def send_signal(sig)
|
148
|
+
return if !running?
|
149
|
+
::Process.kill sig, @run_data.pid
|
80
150
|
end
|
81
151
|
|
82
152
|
end
|
153
|
+
|
83
154
|
end
|
data/lib/scmd/version.rb
CHANGED
data/scmd.gemspec
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "scmd/version"
|
3
5
|
|
4
6
|
Gem::Specification.new do |gem|
|
5
7
|
gem.name = "scmd"
|
6
8
|
gem.version = Scmd::VERSION
|
7
|
-
gem.description = %q{Build and run system commands.}
|
8
|
-
gem.summary = %q{Build and run system commands.}
|
9
|
-
|
10
9
|
gem.authors = ["Kelly Redding", "Collin Redding"]
|
11
10
|
gem.email = ["kelly@kellyredding.com", "collin.redding@me.com"]
|
11
|
+
gem.description = %q{Build and run system commands.}
|
12
|
+
gem.summary = %q{Build and run system commands.}
|
12
13
|
gem.homepage = "http://github.com/redding/scmd"
|
13
14
|
|
14
|
-
gem.files = `git ls-files`.split(
|
15
|
-
gem.
|
16
|
-
gem.
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
18
|
gem.require_paths = ["lib"]
|
18
19
|
|
19
20
|
gem.add_development_dependency("assert")
|
data/test/helper.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# this file is automatically required
|
2
|
-
# put test helpers here
|
1
|
+
# this file is automatically required when you run `assert`
|
2
|
+
# put any test helpers here
|
3
3
|
|
4
4
|
# add root dir to the load path
|
5
5
|
$LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
|
@@ -13,6 +13,9 @@ module Scmd
|
|
13
13
|
|
14
14
|
should have_readers :cmd_str, :pid, :exitstatus, :stdout, :stderr
|
15
15
|
should have_instance_methods :run, :run!
|
16
|
+
should have_instance_methods :start, :wait, :stop, :kill
|
17
|
+
should have_instance_methods :running?, :success?
|
18
|
+
|
16
19
|
|
17
20
|
should "know and return its cmd string" do
|
18
21
|
assert_equal "echo hi", subject.cmd_str
|
@@ -53,7 +56,7 @@ module Scmd
|
|
53
56
|
|
54
57
|
assert_kind_of Scmd::RunError, err
|
55
58
|
assert_includes 'No such file or directory', err.message
|
56
|
-
assert_includes 'test/command_tests.rb:', err.backtrace.first
|
59
|
+
assert_includes 'test/unit/command_tests.rb:', err.backtrace.first
|
57
60
|
end
|
58
61
|
|
59
62
|
should "return itself on `run`, `run!`" do
|
@@ -62,6 +65,42 @@ module Scmd
|
|
62
65
|
assert_equal @failure_cmd, @failure_cmd.run
|
63
66
|
end
|
64
67
|
|
68
|
+
should "start and be running until `wait` is called and the cmd exits" do
|
69
|
+
cmd = Command.new("sleep .1")
|
70
|
+
assert_not cmd.running?
|
71
|
+
|
72
|
+
cmd.start
|
73
|
+
assert cmd.running?
|
74
|
+
assert_not_nil cmd.pid
|
75
|
+
|
76
|
+
cmd.wait
|
77
|
+
assert_not cmd.running?
|
78
|
+
end
|
79
|
+
|
80
|
+
should "do nothing and return when told to wait but not running" do
|
81
|
+
assert_not subject.running?
|
82
|
+
assert_nil subject.pid
|
83
|
+
|
84
|
+
subject.wait
|
85
|
+
assert_nil subject.pid
|
86
|
+
end
|
87
|
+
|
88
|
+
should "do nothing and return when told to stop but not running" do
|
89
|
+
assert_not subject.running?
|
90
|
+
assert_nil subject.pid
|
91
|
+
|
92
|
+
subject.stop
|
93
|
+
assert_nil subject.pid
|
94
|
+
end
|
95
|
+
|
96
|
+
should "do nothing and return when told to kill but not running" do
|
97
|
+
assert_not subject.running?
|
98
|
+
assert_nil subject.pid
|
99
|
+
|
100
|
+
subject.kill
|
101
|
+
assert_nil subject.pid
|
102
|
+
end
|
103
|
+
|
65
104
|
end
|
66
105
|
|
67
106
|
class InputTests < CommandTests
|
@@ -88,4 +127,44 @@ module Scmd
|
|
88
127
|
|
89
128
|
end
|
90
129
|
|
130
|
+
class LongRunningTests < CommandTests
|
131
|
+
desc "that is long running"
|
132
|
+
setup do
|
133
|
+
@long_cmd = Command.new("sleep .3 && echo hi")
|
134
|
+
end
|
135
|
+
|
136
|
+
should "not timeout if wait timeout is longer than cmd time" do
|
137
|
+
assert_nothing_raised do
|
138
|
+
@long_cmd.start
|
139
|
+
@long_cmd.wait(1)
|
140
|
+
end
|
141
|
+
assert @long_cmd.success?
|
142
|
+
assert_equal 'hi', @long_cmd.stdout
|
143
|
+
end
|
144
|
+
|
145
|
+
should "timeout if wait timeout is shorter than cmd time" do
|
146
|
+
assert_raises(TimeoutError) do
|
147
|
+
@long_cmd.start
|
148
|
+
@long_cmd.wait(0.1)
|
149
|
+
end
|
150
|
+
assert_not @long_cmd.success?
|
151
|
+
assert_empty @long_cmd.stdout
|
152
|
+
end
|
153
|
+
|
154
|
+
should "be stoppable" do
|
155
|
+
@long_cmd.start
|
156
|
+
@long_cmd.stop
|
157
|
+
|
158
|
+
assert_not @long_cmd.running?
|
159
|
+
end
|
160
|
+
|
161
|
+
should "be killable" do
|
162
|
+
@long_cmd.start
|
163
|
+
@long_cmd.kill
|
164
|
+
|
165
|
+
assert_not @long_cmd.running?
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
91
170
|
end
|
File without changes
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: scmd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 9
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 2.
|
8
|
+
- 1
|
9
|
+
- 1
|
10
|
+
version: 2.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Kelly Redding
|
@@ -16,11 +16,12 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date:
|
19
|
+
date: 2013-03-12 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
name: assert
|
23
23
|
prerelease: false
|
24
|
+
type: :development
|
24
25
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
26
|
none: false
|
26
27
|
requirements:
|
@@ -30,11 +31,11 @@ dependencies:
|
|
30
31
|
segments:
|
31
32
|
- 0
|
32
33
|
version: "0"
|
33
|
-
type: :development
|
34
34
|
version_requirements: *id001
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: posix-spawn
|
37
37
|
prerelease: false
|
38
|
+
type: :runtime
|
38
39
|
requirement: &id002 !ruby/object:Gem::Requirement
|
39
40
|
none: false
|
40
41
|
requirements:
|
@@ -44,7 +45,6 @@ dependencies:
|
|
44
45
|
segments:
|
45
46
|
- 0
|
46
47
|
version: "0"
|
47
|
-
type: :runtime
|
48
48
|
version_requirements: *id002
|
49
49
|
description: Build and run system commands.
|
50
50
|
email:
|
@@ -59,17 +59,16 @@ extra_rdoc_files: []
|
|
59
59
|
files:
|
60
60
|
- .gitignore
|
61
61
|
- Gemfile
|
62
|
-
- LICENSE
|
62
|
+
- LICENSE.txt
|
63
63
|
- README.md
|
64
64
|
- Rakefile
|
65
65
|
- lib/scmd.rb
|
66
66
|
- lib/scmd/command.rb
|
67
67
|
- lib/scmd/version.rb
|
68
68
|
- scmd.gemspec
|
69
|
-
- test/command_tests.rb
|
70
69
|
- test/helper.rb
|
71
|
-
- test/
|
72
|
-
- test/scmd_tests.rb
|
70
|
+
- test/unit/command_tests.rb
|
71
|
+
- test/unit/scmd_tests.rb
|
73
72
|
homepage: http://github.com/redding/scmd
|
74
73
|
licenses: []
|
75
74
|
|
@@ -99,13 +98,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
99
98
|
requirements: []
|
100
99
|
|
101
100
|
rubyforge_project:
|
102
|
-
rubygems_version: 1.8.
|
101
|
+
rubygems_version: 1.8.25
|
103
102
|
signing_key:
|
104
103
|
specification_version: 3
|
105
104
|
summary: Build and run system commands.
|
106
105
|
test_files:
|
107
|
-
- test/command_tests.rb
|
108
106
|
- test/helper.rb
|
109
|
-
- test/
|
110
|
-
- test/scmd_tests.rb
|
111
|
-
has_rdoc:
|
107
|
+
- test/unit/command_tests.rb
|
108
|
+
- test/unit/scmd_tests.rb
|