command_runner_ng 0.0.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.
- checksums.yaml +7 -0
- data/lib/command_runner.rb +127 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 130d48c47f75bc4bf9cda29964b2ee29f34c1c57
|
4
|
+
data.tar.gz: f63abf2a6127e4ccbe0d669ef75baaeaa93726ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f31d164defc091230024b5b1d6ef0f1a37650d48c3fdab131e8220ea4cfc904aad24c33469ed5d27ffce0744876ef966f0f8e424e355dd03ed3e45a87fd91a9
|
7
|
+
data.tar.gz: 3b7d20c02db29c8ded423ba1471fd7c5b0e2d6775d50541ab5a882366cb485e80c7770d395a0ef36344519b110627a519572d71b782eff1ecab457de60d97d88
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module CommandRunner
|
2
|
+
|
3
|
+
MAX_TIME = Time.new(2**63 -1)
|
4
|
+
|
5
|
+
# Like IO.popen(), but block until the child completes.
|
6
|
+
# Takes an optional timeout parameter. If timeout is a
|
7
|
+
# number the child will be killed after that many seconds
|
8
|
+
# if it haven't completed. Alternatively it can be a Hash
|
9
|
+
# of timeouts to actions. Each action can be a string or integer
|
10
|
+
# specifying the signal to send, or a Proc to execute. The Proc
|
11
|
+
# will be called with the child PID as argument.
|
12
|
+
# These examples are equivalent:
|
13
|
+
# run('sleep 10', timeout: 5) # With a subshell
|
14
|
+
# run(['sleep', '10'], timeout: 5) # No subshell in this one and the rest
|
15
|
+
# run(['sleep', '10'], timeout: {5 => 'KILL'})
|
16
|
+
# run(['sleep', '10'], timeout: {5 => Proc.new { |pid| Process.kill('KILL', pid)}})
|
17
|
+
# run(['sleep', '10'], timeout: {
|
18
|
+
# 5 => 'KILL',
|
19
|
+
# 2 => Proc.new {|pid| puts "PID #{pid} getting SIGKILL in 3s"}
|
20
|
+
# })
|
21
|
+
#
|
22
|
+
# Returns a Hash with :out and :status. :out is a string with stdout
|
23
|
+
# and stderr merged, and :status is a Process::Status.
|
24
|
+
#
|
25
|
+
# As a special case - if an action Proc raises an exception, the child
|
26
|
+
# will be killed with SIGKILL, cleaned up, and the exception rethrown
|
27
|
+
# to the caller of run.
|
28
|
+
#
|
29
|
+
def self.run(*args, timeout: nil)
|
30
|
+
# These could be tweakable through vararg opts
|
31
|
+
tick = 0.1
|
32
|
+
bufsize = 4096
|
33
|
+
|
34
|
+
now = Time.now
|
35
|
+
|
36
|
+
# Build deadline_sequence. A list of deadlines and corresponding actions to take
|
37
|
+
if timeout
|
38
|
+
if timeout.is_a? Numeric
|
39
|
+
deadline_sequence = [{deadline: now + timeout, action: 'KILL'}]
|
40
|
+
elsif timeout.is_a? Hash
|
41
|
+
deadline_sequence = timeout.collect do |t, action|
|
42
|
+
unless action.is_a? Integer or action.is_a? String or action.is_a? Proc
|
43
|
+
raise "Unsupported action type '#{action.class}'. Must be Integer, String, or Proc"
|
44
|
+
end
|
45
|
+
unless t.is_a? Numeric
|
46
|
+
raise "Unsupported timeout value '#{t}'. Must be a Numeric"
|
47
|
+
end
|
48
|
+
{deadline: now + t, action: action}
|
49
|
+
end.sort! { |a, b| a[:deadline] <=> b[:deadline]}
|
50
|
+
else
|
51
|
+
raise "Unsupported type for timeout paramter: #{timeout.class}"
|
52
|
+
end
|
53
|
+
else
|
54
|
+
deadline_sequence = [{deadline: MAX_TIME, action: 0}]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Spawn child, merging stderr into stdout
|
58
|
+
io = IO.popen(*args, :err=>[:child, :out])
|
59
|
+
data = ""
|
60
|
+
|
61
|
+
# Wait until stdout closes
|
62
|
+
while Time.now < deadline_sequence.first[:deadline] do
|
63
|
+
IO.select([io], nil, nil, tick)
|
64
|
+
begin
|
65
|
+
data << io.read_nonblock(bufsize)
|
66
|
+
rescue IO::WaitReadable
|
67
|
+
# Ignore: tick time reached without io
|
68
|
+
rescue EOFError
|
69
|
+
# Child closed stdout (probably dead, but not necessarily)
|
70
|
+
break
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Run through all deadlines until command completes.
|
75
|
+
# We could merge this block into the selecting block above,
|
76
|
+
# but splitting like this saves us a Process.wait syscall per iteration.
|
77
|
+
deadline_sequence.each do |point|
|
78
|
+
while Time.now < point[:deadline]
|
79
|
+
if Process.wait(io.pid, Process::WNOHANG)
|
80
|
+
result = {out: data, status: $?}
|
81
|
+
io.close
|
82
|
+
return result
|
83
|
+
else
|
84
|
+
IO.select([io], nil, nil, tick)
|
85
|
+
begin
|
86
|
+
data << io.read_nonblock(bufsize)
|
87
|
+
rescue IO::WaitReadable
|
88
|
+
# Ignore: tick time reached without io
|
89
|
+
rescue EOFError
|
90
|
+
# Child closed stdout (probably dead, but not necessarily)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Deadline for this point reached. Fire the action.
|
96
|
+
action = point[:action]
|
97
|
+
if action.is_a? String or action.is_a? Integer
|
98
|
+
Process.kill(action, io.pid)
|
99
|
+
elsif action.is_a? Proc
|
100
|
+
begin
|
101
|
+
action.call(io.pid)
|
102
|
+
rescue => e
|
103
|
+
# If the action block throws and error, clean up and rethrow
|
104
|
+
begin
|
105
|
+
Process.kill('KILL', io.pid)
|
106
|
+
rescue
|
107
|
+
# process already dead
|
108
|
+
end
|
109
|
+
Process.wait(io.pid)
|
110
|
+
io.close
|
111
|
+
raise e
|
112
|
+
end
|
113
|
+
else
|
114
|
+
# Given the assertions when building the deadline_sequence this should never be reached
|
115
|
+
raise "Internal error in CommandRunnerNG. Child may be left unattended!"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Either we didn't have a deadline, or none of the deadlines killed of the child.
|
120
|
+
Process.wait(io.pid)
|
121
|
+
result = {out: data, status: $?}
|
122
|
+
io.close
|
123
|
+
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: command_runner_ng
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mikkel Kamstrup Erlandsen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Helper APIs for advanced interactions with subprocesses and shell commands
|
14
|
+
email: mikkel.kamstrup@xamarin.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/command_runner.rb
|
20
|
+
homepage: http://github.com/kamstrup/command_runner_ng
|
21
|
+
licenses:
|
22
|
+
- MIT
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.0.2
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Command Runner NG
|
44
|
+
test_files: []
|