command_runner_ng 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/command_runner.rb +127 -0
  3. 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: []