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 CHANGED
@@ -1,11 +1,10 @@
1
1
  *.gem
2
2
  *.log
3
3
  *.rbc
4
+ .rbx/
4
5
  .bundle
5
6
  .config
6
7
  .yardoc
7
- .rvmrc
8
- .rbenv-version
9
8
  Gemfile.lock
10
9
  InstalledFiles
11
10
  _yardoc
data/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in scmd.gemspec
4
3
  gemspec
5
4
 
6
- gem 'bundler', '~>1.1'
7
- gem 'rake', '~>0.9.2'
5
+ gem 'rake'
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 <TODO: copyright holders>
1
+ Copyright (c) 2012-Present Kelly Redding, Collin Redding
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,20 +1,6 @@
1
1
  # Scmd
2
2
 
3
- Wrapper to `open4` for running system commands.
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
@@ -1,8 +1 @@
1
- #!/usr/bin/env rake
2
-
3
- require 'assert/rake_tasks'
4
- Assert::RakeTasks.install
5
-
6
1
  require 'bundler/gem_tasks'
7
-
8
- task :default => :build
data/lib/scmd.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'scmd/version'
1
2
  require 'scmd/command'
2
3
 
3
4
  module Scmd
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
- reset_results
29
+ setup
25
30
  end
26
31
 
27
- def reset_results
28
- @pid = @exitstatus = nil
29
- @stdout = @stderr = ''
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
- def run(input=nil)
48
- run!(input) rescue RunError
49
- self
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 run!(input=nil)
53
- called_from = caller
126
+ def setup
127
+ @pid = @exitstatus = @run_data = nil
128
+ @stdout = @stderr = ''
129
+ end
54
130
 
55
- begin
56
- pid, stdin, stdout, stderr = POSIX::Spawn::popen4(@cmd_str)
57
- if !input.nil?
58
- [*input].each{|line| stdin.puts line.to_s}
59
- stdin.close
60
- end
61
- @pid = pid.to_i
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
- # `$?` is a thread-safe predefined variable that returns the exit status
72
- # of the last child process to terminate:
73
- # http://phrogz.net/ProgrammingRuby/language.html#predefinedvariables
74
- @exitstatus ||= $?.exitstatus
139
+ def send_term
140
+ send_signal 'TERM'
141
+ end
75
142
 
76
- raise RunError.new(@stderr, called_from) if !success?
77
- end
143
+ def send_kill
144
+ send_signal 'KILL'
145
+ end
78
146
 
79
- self
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
@@ -1,3 +1,3 @@
1
1
  module Scmd
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.1"
3
3
  end
data/scmd.gemspec CHANGED
@@ -1,19 +1,20 @@
1
1
  # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/scmd/version', __FILE__)
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("\n")
15
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
- gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
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 in when you require 'assert' in your tests
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: 15
4
+ hash: 9
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
- - 0
9
- - 0
10
- version: 2.0.0
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: 2012-11-08 00:00:00 Z
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/irb.rb
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.15
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/irb.rb
110
- - test/scmd_tests.rb
111
- has_rdoc:
107
+ - test/unit/command_tests.rb
108
+ - test/unit/scmd_tests.rb
data/test/irb.rb DELETED
@@ -1,9 +0,0 @@
1
- require 'assert/setup'
2
-
3
- # this file is required in when the 'irb' rake test is run.
4
- # b/c 'assert/setup' is required above, the test helper will be
5
- # required in as well.
6
-
7
- # put any IRB setup code here
8
-
9
- require 'scmd'