command_runner_ng 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/command_runner.rb +74 -8
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6de27ba13c1082bff813ee1eb2aec4c84dfe69f
|
4
|
+
data.tar.gz: 48227b83ec2908c902859d7468f10cf55bbd6975
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b90ebd4992561b21a8b83c1de423254f8d114a97e79723d2b37f9a631380e9809689995d3c0b1a9a4ea98b745a870d37018429ec9be273fb764cecee78c0d9ce
|
7
|
+
data.tar.gz: d0e854031e48f9fa0f06c7f220ecadc9bd31ae98bbd041d346b3346ff3774c7f6b6a0badffb3505d4c816ccc3f06c5f06ba62ddcd3ed886a038fe14b67accfcd
|
data/lib/command_runner.rb
CHANGED
@@ -3,22 +3,31 @@ module CommandRunner
|
|
3
3
|
MAX_TIME = Time.new(2**63 -1)
|
4
4
|
|
5
5
|
# Like IO.popen(), but block until the child completes.
|
6
|
+
#
|
7
|
+
# For convenience allows you to pass > 1 string args without a boxing array,
|
8
|
+
# to execute a command with arguments without a subshell. See examples below.
|
9
|
+
#
|
6
10
|
# Takes an optional timeout parameter. If timeout is a
|
7
11
|
# number the child will be killed after that many seconds
|
8
12
|
# if it haven't completed. Alternatively it can be a Hash
|
9
13
|
# of timeouts to actions. Each action can be a string or integer
|
10
14
|
# specifying the signal to send, or a Proc to execute. The Proc
|
11
15
|
# will be called with the child PID as argument.
|
16
|
+
#
|
12
17
|
# These examples are equivalent:
|
13
18
|
# run('sleep 10', timeout: 5) # With a subshell
|
19
|
+
# run('sleep', '10', timeout: 5) # Without subshell. Convenience API to avoid array boxing as below
|
14
20
|
# run(['sleep', '10'], timeout: 5) # No subshell in this one and the rest
|
15
21
|
# run(['sleep', '10'], timeout: {5 => 'KILL'})
|
16
22
|
# run(['sleep', '10'], timeout: {5 => Proc.new { |pid| Process.kill('KILL', pid)}})
|
17
23
|
# run(['sleep', '10'], timeout: {
|
18
24
|
# 5 => 'KILL',
|
19
|
-
# 2 => Proc.new {|pid| puts "PID #{pid}
|
25
|
+
# 2 => Proc.new {|pid| puts "PID #{pid} geting SIGKILL in 3s"}
|
20
26
|
# })
|
21
27
|
#
|
28
|
+
# Takes an optional environment parameter (a Hash). The environment is
|
29
|
+
# populated with the keys/values of this parameter.
|
30
|
+
#
|
22
31
|
# Returns a Hash with :out and :status. :out is a string with stdout
|
23
32
|
# and stderr merged, and :status is a Process::Status.
|
24
33
|
#
|
@@ -26,7 +35,12 @@ module CommandRunner
|
|
26
35
|
# will be killed with SIGKILL, cleaned up, and the exception rethrown
|
27
36
|
# to the caller of run.
|
28
37
|
#
|
29
|
-
def self.run(*args, timeout: nil)
|
38
|
+
def self.run(*args, timeout: nil, environment: {})
|
39
|
+
# If args is an array of strings, allow that as a shorthand for [arg1, arg2, arg3]
|
40
|
+
if args.length > 1 && args.all? {|arg| arg.is_a? String}
|
41
|
+
args = [args]
|
42
|
+
end
|
43
|
+
|
30
44
|
# This could be tweakable through vararg opts
|
31
45
|
tick = 0.1
|
32
46
|
|
@@ -35,7 +49,7 @@ module CommandRunner
|
|
35
49
|
# Build deadline_sequence. A list of deadlines and corresponding actions to take
|
36
50
|
if timeout
|
37
51
|
if timeout.is_a? Numeric
|
38
|
-
deadline_sequence = [{deadline
|
52
|
+
deadline_sequence = [{:deadline => now + timeout, :action => 'KILL'}]
|
39
53
|
elsif timeout.is_a? Hash
|
40
54
|
deadline_sequence = timeout.collect do |t, action|
|
41
55
|
unless action.is_a? Integer or action.is_a? String or action.is_a? Proc
|
@@ -44,17 +58,17 @@ module CommandRunner
|
|
44
58
|
unless t.is_a? Numeric
|
45
59
|
raise "Unsupported timeout value '#{t}'. Must be a Numeric"
|
46
60
|
end
|
47
|
-
{deadline
|
61
|
+
{:deadline => now + t, :action => action}
|
48
62
|
end.sort! { |a, b| a[:deadline] <=> b[:deadline]}
|
49
63
|
else
|
50
64
|
raise "Unsupported type for timeout paramter: #{timeout.class}"
|
51
65
|
end
|
52
66
|
else
|
53
|
-
deadline_sequence = [{deadline
|
67
|
+
deadline_sequence = [{:deadline => MAX_TIME, :action => 0}]
|
54
68
|
end
|
55
69
|
|
56
70
|
# Spawn child, merging stderr into stdout
|
57
|
-
io = IO.popen(*args, :err=>[:child, :out])
|
71
|
+
io = IO.popen(environment, *args, {:err=>[:child, :out]})
|
58
72
|
data = ""
|
59
73
|
|
60
74
|
# Run through all deadlines until command completes.
|
@@ -65,7 +79,7 @@ module CommandRunner
|
|
65
79
|
while Time.now < point[:deadline]
|
66
80
|
if Process.wait(io.pid, Process::WNOHANG)
|
67
81
|
read_nonblock_safe!(io, data, tick)
|
68
|
-
result = {out
|
82
|
+
result = {:out => data, :status => $?}
|
69
83
|
io.close
|
70
84
|
return result
|
71
85
|
elsif !eof
|
@@ -100,14 +114,66 @@ module CommandRunner
|
|
100
114
|
# Either we didn't have a deadline, or none of the deadlines killed off the child.
|
101
115
|
Process.wait(io.pid)
|
102
116
|
read_nonblock_safe!(io, data, tick)
|
103
|
-
result = {out
|
117
|
+
result = {:out => data, :status => $?}
|
104
118
|
io.close
|
105
119
|
|
106
120
|
result
|
107
121
|
end
|
108
122
|
|
123
|
+
# Create a helper instance to launch a command with a given configuration.
|
124
|
+
# Invoke the command with the run() method. The configuration given to create()
|
125
|
+
# can be overriden on each invocation of run().
|
126
|
+
#
|
127
|
+
# The run() method of the helper instance must be invoked with a
|
128
|
+
#
|
129
|
+
# Examples:
|
130
|
+
# git = CommandRunner.create(['sudo', 'git'], timeout: 10, allowed_sub_commands: [:commit, :pull, :push])
|
131
|
+
# git.run(:pull, 'origin', 'master')
|
132
|
+
# git.run(:pull, 'origin', 'master', timeout: 2) # override default timeout of 10
|
133
|
+
# git.run(:status) # will raise an error because :status is not in list of allowed commands
|
134
|
+
def self.create(*args, timeout: nil, environment: {}, allowed_sub_commands: [])
|
135
|
+
CommandInstance.new(args, timeout, environment, allowed_sub_commands)
|
136
|
+
end
|
137
|
+
|
109
138
|
private
|
110
139
|
|
140
|
+
class CommandInstance
|
141
|
+
|
142
|
+
def initialize(default_args, default_timeout, default_environment, allowed_sub_commands)
|
143
|
+
unless default_args.first.is_a? Array
|
144
|
+
raise "First argument must be an array of command line args. Found #{default_args}"
|
145
|
+
end
|
146
|
+
|
147
|
+
@default_args = default_args
|
148
|
+
@default_timeout = default_timeout
|
149
|
+
@default_environment = default_environment
|
150
|
+
@allowed_sub_commands = allowed_sub_commands
|
151
|
+
end
|
152
|
+
|
153
|
+
def run(*args, timeout: nil, environment: {})
|
154
|
+
args_list = *args
|
155
|
+
|
156
|
+
if !args_list.nil? && !args_list.empty? && args_list.first.is_a?(Array)
|
157
|
+
if args_list.length > 1
|
158
|
+
raise "Unsupported args list length: #{args_list.length}"
|
159
|
+
else
|
160
|
+
args_list = args_list.first
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Check sub command if needed
|
165
|
+
if !args_list.nil? && !args_list.empty? &&
|
166
|
+
!@allowed_sub_commands.empty? && !@allowed_sub_commands.include?(args_list.first)
|
167
|
+
raise "Illegal sub command '#{args_list.first}'. Expected #{allowed_sub_commands} (#{allowed_sub_commands.include?(args_list.first)})"
|
168
|
+
end
|
169
|
+
|
170
|
+
full_args = @default_args.dup
|
171
|
+
full_args[0] += args_list.map {|arg| arg.to_s }
|
172
|
+
CommandRunner.run(*full_args, timeout: (timeout || @default_timeout), environment: @default_environment.merge(environment))
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
111
177
|
# Read data async, appending to data_out,
|
112
178
|
# returning true on EOF, false otherwise
|
113
179
|
def self.read_nonblock_safe!(io, data_out, tick)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: command_runner_ng
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mikkel Kamstrup Erlandsen
|
@@ -29,7 +29,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
30
30
|
- - '>='
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 2.0.0
|
33
33
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
34
|
requirements:
|
35
35
|
- - '>='
|