scmd 2.0.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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'